1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 18:26:26 +00:00

Compare commits

...

442 Commits

Author SHA1 Message Date
Dick Choi
7faf0efb20 Merge branch '0.6.0' 2016-07-24 03:12:15 +09:00
Dick Choi
ed70cb8e3d fix forever loading if user is new 2016-07-24 03:11:07 +09:00
Dick Choi
2544b5b821 fix path validation for Windows 2016-07-23 23:47:07 +09:00
Rokt33r
90613b7cc8 Merge branch '0.6.0' 2016-07-23 20:44:07 +09:00
Rokt33r
173640d0b4 ready to pack 2016-07-23 20:28:01 +09:00
Dick Choi
e70d09ebbf modify smaller border for folder item 2016-07-23 15:52:07 +09:00
Dick Choi
1ff33378e1 esc to close modal 2016-07-23 15:45:21 +09:00
Dick Choi
286739f770 cleanup 2016-07-23 15:41:30 +09:00
Dick Choi
45b1cd3942 delete note 2016-07-23 15:28:17 +09:00
Dick Choi
b2d34ab95d add initialization modal & migration func 2016-07-23 12:14:01 +09:00
Dick Choi
f628ed55cf update infotab 2016-07-23 01:41:31 +09:00
Dick Choi
70ec9f50ab disable finder temporary 2016-07-22 08:45:13 +09:00
Dick Choi
d6d130b8f9 app won't quit before Ctrl+ Q 2016-07-22 08:45:04 +09:00
Dick Choi
40410eb10f hotkey config 2016-07-22 08:39:25 +09:00
Dick Choi
a6e3dbd825 change folder 2016-07-21 23:44:00 +09:00
Dick Choi
3882df41f1 fix tag search bug 2016-07-21 22:51:18 +09:00
Dick Choi
6c67b96e30 search 2016-07-21 03:21:53 +09:00
Dick Choi
b6d34472fe snippet note 2016-07-21 01:17:53 +09:00
Dick Choi
69d11b5cd0 fix css bugs
content of rendered preview should be selectable
the hit box of width slider should has the highest z-index
2016-07-20 00:47:25 +09:00
Dick Choi
f36f2029e5 change layout 2016-07-19 13:29:05 +09:00
Dick Choi
af7476f79a use flex layout to center modal 2016-07-19 13:28:18 +09:00
Dick Choi
890207daee auto scroll 2016-07-19 08:59:47 +09:00
Dick Choi
9a5653f1e3 preview on blur 2016-07-19 02:38:23 +09:00
Dick Choi
4f45ba1680 embed navigating, filterring to notelist 2016-07-15 10:34:52 +09:00
Dick Choi
0ccfd36a83 smaller storage name 2016-07-15 10:21:34 +09:00
Dick Choi
8452a95114 hide sidenav toggle button temporarily 2016-07-15 10:21:21 +09:00
Dick Choi
9b98035ee7 smaller padding of code block 2016-07-14 14:42:20 +09:00
Dick Choi
6dc75afdc8 Merged branch newv0.6.0 into 0.6.0 2016-07-14 14:37:45 +09:00
Dick Choi
922ae7a274 clean old codes 2016-07-14 14:37:26 +09:00
Dick Choi
2d46d12628 Codeblock theme of markdown preview 2016-07-14 14:37:14 +09:00
Dick Choi
44f270f408 rewite whole code
add dataApi
renew PreferencesModal
2016-07-14 13:58:14 +09:00
Dick Choi
9ff70c4aef display updater error logs 2016-07-14 13:57:09 +09:00
Dick Choi
8c6a54397f Merge branch 'no_submodules'
Conflicts:
	readme.md
2016-06-12 20:01:20 +09:00
Dick Choi
ce915df2b2 focus content editor after create new Note 2016-06-12 19:55:39 +09:00
Dick Choi
84dd772b7c v0.5.13 2016-06-07 11:04:25 +09:00
Dick Choi
959f6c2c58 no more submodules 2016-06-07 10:59:39 +09:00
Dick Choi
793b2b20e9 Merged branch master into master 2016-06-07 10:53:07 +09:00
Dick Choi
1c224fedc5 hide scroll bar when modal shows up 2016-06-07 10:48:41 +09:00
Dick Choi
4299baec5d enable devtools 2016-06-07 10:48:20 +09:00
Dick Choi
e931d3efbc Merge pull request #43 from eschmid72/master
including ace via npm, not as git submodule anymore
2016-06-06 10:41:00 +09:00
Egon schmid
36d81a6ec9 updated package.json and gruntfile.js 2016-06-05 12:33:02 +02:00
Egon schmid
22c2d09d88 changing git@github.com/ajaxorg/ace.git tp git@github.com/ajaxorg/ace-builds.git 2016-06-05 12:19:14 +02:00
Egon schmid
558659b54d including ace via npm, not as git submodule anymore 2016-06-05 11:44:26 +02:00
Dick Choi
eb1a0ba49f switch note by arrow buttons 2016-06-01 11:50:58 +09:00
Dick Choi
45212e7e14 redirect when create new note 2016-06-01 10:19:36 +09:00
Dick Choi
c691af9712 debug autoscroll 2016-06-01 09:35:48 +09:00
Dick Choi
0c2226b3fc cancel dispatch queue before focrcing & shorter dispatch latency 2016-05-31 21:39:42 +09:00
Dick Choi
eeda6b0208 remove unnecessary log 2016-05-31 21:38:47 +09:00
Dick Choi
d9096424c5 set value when updating 2016-05-31 21:38:34 +09:00
Dick Choi
d171078ca4 fix z-index of option 2016-05-31 21:37:06 +09:00
Dick Choi
cb18b8f347 fix style 2016-05-31 14:29:49 +09:00
Dick Choi
4fdb72f93c add FolderSelect 2016-05-31 14:29:01 +09:00
Dick Choi
549930b48c Enhance Tag style 2016-05-29 14:15:43 +09:00
Dick Choi
409a3de901 update NoteList
sort by updatedAt
improve style
add auto-scrolling
2016-05-29 14:02:23 +09:00
Dick Choi
431ac5a403 prevent duplication of starred 2016-05-29 13:52:17 +09:00
Dick Choi
999d4633ab add readme-ja.md 2016-05-29 13:14:18 +09:00
Dick Choi
5a26fc812d Star a note 2016-05-29 13:08:02 +09:00
Dick Choi
eb210e9072 update readme 2016-05-29 12:12:10 +09:00
Dick Choi
dbfd25bb8e value attribute of Editor must be set after mounting 2016-05-29 02:54:39 +09:00
Dick Choi
4292583d73 TagSelect 2016-05-29 02:54:01 +09:00
Dick Choi
7c346d62eb remove old TagSelect 2016-05-29 02:53:28 +09:00
Dick Choi
14abbee3fb fix item of NoteList
add key to tag
replace createdAt with updatedAt
2016-05-29 02:27:41 +09:00
Dick Choi
903979b3f2 set updatedAt when dispatching SAVE_NOTE 2016-05-29 02:26:26 +09:00
Dick Choi
913e649e5d fix overflow style of item in folded SideNav 2016-05-29 01:09:32 +09:00
Dick Choi
7f8733796e Detail 2016-05-25 17:11:10 +09:00
Dick Choi
1a98afee92 upgrade katex 2016-05-25 17:07:59 +09:00
Dick Choi
60daafe136 clean lib 2016-05-25 17:07:41 +09:00
Dick Choi
c7ad06a1f7 Update NoteList
redirect if no note selected
add overflow style of labels in Note item
2016-05-25 02:16:22 +09:00
Dick Choi
d07c795511 Navigating by notelist 2016-05-24 20:15:52 +09:00
Dick Choi
89d1071dc1 update StatusBar
fix width of URL status
show dialog before update
2016-05-24 20:14:53 +09:00
Dick Choi
dfe34a9f22 update webpack.config 2016-05-24 16:53:21 +09:00
Dick Choi
93d3ea70fc handle new note button & note list 2016-05-22 23:44:49 +09:00
Rokt33r
89a76d9ead resizible list 2016-05-19 13:58:39 +09:00
Rokt33r
dc6bd1aae8 bump up electron 1.0.2 2016-05-14 19:03:47 +09:00
Rokt33r
3412d70737 set minimum size to main-window 2016-05-14 19:03:27 +09:00
Rokt33r
ce52d5cf42 improve style of StatusBar & display pathname from StatusBar 2016-05-14 19:03:05 +09:00
Rokt33r
da19066fb8 update style of buttons in SideNav 2016-05-14 19:01:57 +09:00
Rokt33r
be20c2c800 renew TopBar 2016-05-14 19:01:32 +09:00
Rokt33r
c59638aaae Don't call devtron every refreshing 2016-05-14 19:00:45 +09:00
Rokt33r
00691f1225 shorter uid 2016-05-14 18:47:09 +09:00
Rokt33r
735b79a4e0 refactor ZoomSelect iteration 2016-05-14 15:52:05 +09:00
Rokt33r
443b529667 extract stylus vars of nav 2016-05-14 13:50:23 +09:00
Rokt33r
c3e62f58ab remove menu offset 2016-05-14 12:47:59 +09:00
Rokt33r
9869545777 remove linkState function & move modal.js to main/lib 2016-05-14 12:45:49 +09:00
Rokt33r
04e5034f5f add StatusBar to Main 2016-05-14 12:45:07 +09:00
Rokt33r
e99e3eb6c4 update readme 2016-05-14 12:29:53 +09:00
Rokt33r
508615b600 add StatusBar & Zoom UI 2016-05-14 12:29:38 +09:00
Rokt33r
4d3d416ecb use ConfigManager 2016-05-14 12:27:28 +09:00
Rokt33r
be2897806a show empty warning properly when SideNav folded 2016-05-14 12:26:11 +09:00
Rokt33r
6ebde04d22 add ConfigManager & ZoomManager 2016-05-14 12:24:24 +09:00
Rokt33r
9deaa8694e simplify keygen 2016-05-14 12:23:49 +09:00
Rokt33r
5dff50ff92 upgrade electron 2016-05-13 03:20:35 +09:00
Rokt33r
ba4d90765a devtron 2016-05-13 03:17:08 +09:00
Dick Choi
187ccaa95e Update readme.md 2016-05-12 21:04:00 +09:00
Rokt33r
18b6d8289f refactor file structure 2016-05-12 21:02:40 +09:00
Rokt33r
c851f8f006 modify localIdentName of css loader 2016-05-12 20:45:03 +09:00
Rokt33r
9d60704c9b Merge branch 'master'
Conflicts:
	package.json
2016-05-12 20:10:18 +09:00
Rokt33r
365d7a1afd foldable SideNav 2016-05-12 19:53:21 +09:00
Rokt33r
a3d5f23861 update sidebar style
add transition
modify margin among menu buttons
2016-05-11 09:59:17 +09:00
Rokt33r
330ea986d4 use ReactRouter 2016-05-08 14:42:38 +09:00
Rokt33r
652c1aae73 re-color style of SideNav 2016-05-08 14:42:08 +09:00
Rokt33r
c91adcd165 update label 2016-05-08 14:41:27 +09:00
Rokt33r
293c286d22 update color option and enhance UX of FolderItem 2016-05-08 03:03:26 +09:00
Rokt33r
70244f79ba rename, recolor, remove folder 2016-05-08 02:49:19 +09:00
Rokt33r
6ba49ad294 fix line-height 2016-05-04 01:59:11 +09:00
Rokt33r
acf8cbfe0e bump up to v0.5.12 2016-05-04 00:56:24 +09:00
Rokt33r
f904fd00e5 add code block line numbering option to 2016-05-04 00:47:22 +09:00
Rokt33r
831bec5baf brand new ace 2016-05-03 23:35:58 +09:00
Dick Choi
f0257b0f87 Merge pull request #28 from eschmid72/master
displaying markdown codeblocks with line numbering
2016-05-03 23:32:15 +09:00
Rokt33r
66856d1229 clean old codes 2016-05-03 23:21:27 +09:00
Rokt33r
27c22a4b09 focus first input & fix deprecated code 2016-05-03 23:18:39 +09:00
Rokt33r
1e7cce9056 update style of SideNav 2016-05-03 23:05:44 +09:00
Rokt33r
219af76679 set Input disabled when saving 2016-05-03 23:03:04 +09:00
Rokt33r
dcf773fd88 add Folder 2016-05-03 22:38:54 +09:00
Rokt33r
df51e6d429 renew SideNav 2016-05-03 17:54:09 +09:00
Rokt33r
6ea66b85e0 update style variables 2016-05-03 17:50:28 +09:00
Rokt33r
10aae90ae2 clean code 2016-05-03 15:41:11 +09:00
Rokt33r
ea8e31561e add oh-my-cdn & vendor script 2016-05-03 15:38:23 +09:00
Rokt33r
45c627b0a5 using Repository class 2016-05-03 15:37:14 +09:00
Rokt33r
1d2ca469fc add Repository class 2016-05-03 15:33:47 +09:00
Rokt33r
e6a0c86f4e upgrade ace 2016-05-02 04:37:39 +09:00
Rokt33r
039429d452 add merge-stream to devDependencies 2016-05-02 04:37:33 +09:00
Rokt33r
46df9398e8 remove OSSAnnounceModal 2016-05-02 04:36:59 +09:00
Rokt33r
744907ac38 remove repo 2016-05-02 04:36:34 +09:00
Egon schmid
084677cdca code style 2016-04-30 21:30:52 +02:00
Egon schmid
891cd3124f added gitter for codeblocks 2016-04-29 18:30:52 +02:00
Rokt33r
b9fd3b8b7d Merge v0.5.11
Conflicts:
	browser/main/index.js
	package.json
2016-04-29 12:25:55 +09:00
Rokt33r
f6208c1324 bump up to v0.5.11 2016-04-26 14:35:57 +09:00
Rokt33r
3a117c0f09 Modify labels in theme section(much easier to understand) 2016-04-26 14:35:47 +09:00
Dick Choi
a0c83f33ca Merge pull request #26 from dojineko/hotfix-dark
Hotfix for dark theme
2016-04-26 12:52:40 +09:00
dojineko
99b9fadd74 Adjust folder name position of list item 2016-04-25 23:16:39 +09:00
dojineko
cf023847ad Apply dark theme to document body 2016-04-25 23:05:50 +09:00
dojineko
59357b274d Apply dark theme to folder names of list item 2016-04-25 22:56:52 +09:00
dojineko
1582184223 Apply dark theme to spans of code 2016-04-25 22:51:35 +09:00
dojineko
db9bcafb82 Remove border-radius from CodeEditor 2016-04-25 21:53:01 +09:00
dojineko
50b9838eec Apply dark theme to delete article modal 2016-04-25 21:52:43 +09:00
Dick Choi
ff958b7cd6 update contributors 2016-04-25 15:27:57 +09:00
Dick Choi
33bc2aa220 enable Finder on Cinnamon 2016-04-25 15:26:24 +09:00
dojineko
03b331e5d5 Apply dark theme to markdown table 2016-04-25 10:36:05 +09:00
Rokt33r
86cc9fb7d8 bump up to v0.5.10 & add Contributors 2016-04-23 14:52:53 +09:00
Rokt33r
f98efe0b97 fix changelog link 2016-04-23 14:47:46 +09:00
Rokt33r
6c5345df64 disable asar & ignore node_modules/grunt 2016-04-23 13:03:10 +09:00
Dick Choi
2d97661d28 set grunt-electron-installer-debian optional 2016-04-23 12:13:57 +09:00
Dick Choi
3efe0c1ce2 use forked markdown-it-math 2016-04-23 11:20:37 +09:00
Rokt33r
6099c91216 standarded 2016-04-21 21:37:43 +09:00
Rokt33r
934e4d9607 Enhance style & Fix bug 2016-04-21 21:03:28 +09:00
Dick Choi
1198f6112e Merge pull request #20 from dojineko/dark-theme
Add dark theme
2016-04-21 20:03:11 +09:00
Rokt33r
ac7e08ae2c Redesign SideNav & implement add repo func 2016-04-18 15:36:02 +09:00
Rokt33r
9c5cbd348b Wrap CSSModules 2016-04-18 12:44:52 +09:00
Rokt33r
8600710d23 global styles must not use pre-loader 2016-04-18 12:44:32 +09:00
Rokt33r
d06d52deda ready to use CSSModules 2016-04-18 12:43:34 +09:00
Rokt33r
948377ba93 Use browser target dist of react stuff
for faster compile
2016-04-18 05:47:13 +09:00
Rokt33r
d878028dcc add season & css modules 2016-04-18 04:39:53 +09:00
Rokt33r
65c969a321 move devtool to main-menu 2016-04-18 04:37:00 +09:00
dojineko
777f7f9305 Add code theme selector 2016-04-17 21:52:55 +09:00
dojineko
43fb37ab1d Add dark theme and UI theme selector 2016-04-17 21:52:50 +09:00
dojineko
06734ec886 Add syntax theme selector 2016-04-17 21:03:27 +09:00
dojineko
0b8ae93727 Fix border shift 2016-04-17 21:03:27 +09:00
dojineko
24cea97e08 Replace single quote to entity reference 2016-04-16 13:58:05 +09:00
dojineko
18536e5db0 Ignore meta files 2016-04-16 13:16:45 +09:00
Rokt33r
8c236cb5cb add testing targets 2016-04-12 14:46:14 +09:00
Rokt33r
797c8ad7fa update readme.md 2016-04-12 13:47:37 +09:00
Rokt33r
ba8d2dcb8b add more option to gruntfile for installer-debian 2016-04-12 11:06:20 +09:00
Rokt33r
78a095d958 bumpup to 0.5.9 2016-04-11 08:08:52 +09:00
Rokt33r
627172f6df discard wrong error notification(There is no newer version.) 2016-04-11 08:08:42 +09:00
Dick Choi
a39f25961c reorganize menu for Linux 2016-04-07 08:35:51 +09:00
Dick Choi
e738ae5c8c Add more info to gruntfile for Linux app 2016-04-07 08:35:40 +09:00
Dick Choi
749e85e8e6 remove a depreciated option of mainWIndow 2016-04-07 08:22:54 +09:00
Dick Choi
cfa251b158 Disable Finder and Auto-update on Linux 2016-04-07 08:19:04 +09:00
Dick Choi
1ca38b8741 Update package urls 2016-04-04 08:03:32 +09:00
Dick Choi
d7bc5a7088 ready for linux app :) 2016-04-04 08:02:53 +09:00
Dick Choi
3b9a2c3ee1 Merged branch linux into master 2016-04-04 07:29:40 +09:00
Dick Choi
260ad77d39 Merged branch linux into master 2016-04-04 07:02:39 +09:00
Dick Choi
5c508a0cd9 Merge pull request #10 from brpaz/linux
Add support for creating .deb file on linux
2016-04-04 06:28:23 +09:00
Dick Choi
fd3d1607a4 Merge pull request #11 from brpaz/master
Fix case on style import
2016-04-02 23:38:26 +09:00
Bruno Paz
abbc0fbcf1 Fix case on style import 2016-04-02 14:09:23 +01:00
Bruno Paz
00be41608d Add support for creating .deb file on linux 2016-04-02 13:25:54 +01:00
Rokt33r
499c3f2e13 bump up version to 0.5.8 and upgrade electron packager 2016-04-02 20:45:45 +09:00
Rokt33r
627845f6e4 fix file drop behaviour 2016-04-02 20:15:14 +09:00
Rokt33r
07eea76057 fix editor selection bug 2016-04-02 20:14:47 +09:00
Rokt33r
fecc4e9b79 bump up to v0.5.7 2016-03-28 01:26:11 +09:00
Rokt33r
58e9302f15 remove email address 2016-03-28 01:09:43 +09:00
Rokt33r
86f649fab1 v0.5.6 2016-03-27 23:18:54 +09:00
Dick Choi
9ac0becfb2 Update readme.md 2016-03-26 03:53:13 +09:00
Rokt33r
eda547b868 Merge branch 'master' into linux
* master:
  configuring zoomfactor with Ctrl + Wheelscroll only works on windows and linux
  fix style bug of TagSelect
  bump up electron version to  0.36.11
  fix again(never break long tag)
  tags in ArticleList will be wrapped properly
  fix emacs key binding
  ctrl +  wheel to change zoom factor
  set Editor fontFamily to Title input
  add Link of Trello Kanban to readme.md
  Update contributing
  Focus to search box, when hotkey pushed
2016-03-16 16:52:17 +09:00
Rokt33r
f75e872415 configuring zoomfactor with Ctrl + Wheelscroll only works on windows and linux 2016-03-16 08:08:42 +09:00
Rokt33r
aef0712165 fix style bug of TagSelect 2016-03-16 08:02:46 +09:00
Rokt33r
bed4b7fd27 bump up electron version to 0.36.11 2016-03-15 16:17:39 +09:00
Rokt33r
b53ff5daf3 fix again(never break long tag) 2016-03-15 16:09:17 +09:00
Rokt33r
bb0872b4fc tags in ArticleList will be wrapped properly 2016-03-15 15:51:35 +09:00
Rokt33r
b65101f4be fix emacs key binding 2016-03-15 14:05:33 +09:00
Rokt33r
593d242a4c ctrl + wheel to change zoom factor 2016-03-15 02:24:28 +09:00
Rokt33r
db7f339c34 set Editor fontFamily to Title input 2016-03-15 01:32:31 +09:00
Rokt33r
9f3575a874 Merge branch 'master' into v0.5.5
* master:
  add Link of Trello Kanban to readme.md
2016-03-13 23:04:08 +09:00
Rokt33r
1c9c59c512 add Link of Trello Kanban to readme.md 2016-03-13 23:03:50 +09:00
Rokt33r
127202b831 Update contributing 2016-03-10 20:32:59 +09:00
Dick Choi
4f8a04ed21 Merge pull request #1 from dojineko/quick-search
Top search will be focused when Main window showed by hotkey.
2016-03-10 19:44:57 +09:00
dojineko
63b2e0560b Focus to search box, when hotkey pushed 2016-03-10 08:49:44 +09:00
Rokt33r
07291d71f2 add comment for linux version 2016-03-09 20:34:16 +09:00
Rokt33r
d1ca1ec4d9 Merge branch 'v0.5.5' into linux
* v0.5.5:
  The notification of 'Copy to Clipboard' must be slient
  modify window API to fit the new version of electron
  bump up version of electron
2016-03-09 20:03:07 +09:00
Rokt33r
6907cf9972 The notification of 'Copy to Clipboard' must be slient 2016-03-09 19:43:34 +09:00
Rokt33r
d4f8d1498d modify window API to fit the new version of electron 2016-03-09 19:42:45 +09:00
Rokt33r
0952e4a664 bump up version of electron 2016-03-09 03:16:40 +09:00
Rokt33r
6a4e8c95ea Merge branch 'master' into linux
* master:
  fix contributing
  add contributing
  modify readme
  modify LICENSE and readme
2016-03-09 02:52:06 +09:00
Rokt33r
983bfb7adf fix contributing 2016-03-09 02:50:33 +09:00
Rokt33r
d7aaf5e210 add contributing 2016-03-09 02:45:07 +09:00
Rokt33r
50281132ad modify readme 2016-03-09 02:44:57 +09:00
Rokt33r
6a2b22015e modify LICENSE and readme 2016-03-09 02:44:38 +09:00
Rokt33r
2f90890f50 setup gruntfile for linux deploy & disable finder and updater 2016-03-09 01:57:36 +09:00
Rokt33r
0fe83a0583 fix anchor target 2016-01-25 10:28:46 +09:00
Rokt33r
ce74e69480 check href exists 2016-01-22 08:00:26 +09:00
Rokt33r
ddea2aeb22 fix verticla-align of img in anchor 2016-01-22 07:58:25 +09:00
Rokt33r
7bbe69cce9 Merge branch 'v0.5.4'
* v0.5.4:
  bump up version to v0.5.4
  emit ARTICLE_CREATE when new post clicked
  update h1 style of markdown
  fix go to line end
  clean old code
  fix sanitizing bug in Code block
  done but bugged

Conflicts:
	package.json
2016-01-22 03:01:13 +09:00
Rokt33r
e921e30d64 bump up version to v0.5.4 2016-01-22 02:59:35 +09:00
Rokt33r
cd4f9d8bb4 emit ARTICLE_CREATE when new post clicked 2016-01-22 02:58:06 +09:00
Rokt33r
a0553788b6 update h1 style of markdown 2016-01-22 02:55:02 +09:00
Rokt33r
1a183d78af fix go to line end 2016-01-21 08:29:09 +09:00
Rokt33r
cabcaa892c clean old code 2016-01-21 05:50:53 +09:00
Rokt33r
01c9d62a2b fix sanitizing bug in Code block 2016-01-21 05:50:40 +09:00
Rokt33r
ba76df863c bump up version 2016-01-19 20:13:17 +09:00
Rokt33r
81441a0895 done but bugged 2016-01-19 20:11:38 +09:00
Rokt33r
da0222f213 esc to show preview when rightclick to toggle set 2016-01-19 18:58:27 +09:00
Rokt33r
fb8a2eb2e0 show proper error when hotkey updated 2016-01-19 18:47:35 +09:00
Rokt33r
cde2e27e04 must override default config 2016-01-19 18:47:10 +09:00
Dick Choi
3758ea2cf4 bump up version 2016-01-18 01:02:05 +09:00
Rokt33r
e62fc11328 switch folder properly after moving an article to other folder 2016-01-17 08:43:01 +09:00
Dick Choi
3cbfae83c1 fix basefonts again 2016-01-17 07:27:38 +09:00
Dick Choi
57667654ef add Base fonts for windows 2016-01-17 00:19:20 +09:00
Rokt33r
eadd66fa91 decode entities to parse properly by katex 2016-01-16 19:40:03 +09:00
Rokt33r
75cd94a39a refactor MarkdownPreview & protocol must be defined in 'href' to open in browser 2016-01-16 05:28:27 +09:00
Rokt33r
7872bfe19d Merge branch 'markdown' into v0.5.2
* markdown:
  markdown strike bug fixed, no emoji shortcut, checkbox syntax added
2016-01-16 04:54:02 +09:00
Rokt33r
af008e69c2 modify default hotkey(toggle-main-window) 2016-01-16 04:53:49 +09:00
Rokt33r
a549abc20f Refactor event handlers in ArticleEditor 2016-01-16 04:53:06 +09:00
Rokt33r
116344737a right click to quick preview(edit on focus) 2016-01-16 04:24:09 +09:00
Rokt33r
93c03f4e88 add right click to switch edit/preview option to app settings 2016-01-16 02:40:31 +09:00
Rokt33r
445332c27c Find command of ace won't fire blur 2016-01-16 00:58:49 +09:00
Rokt33r
c42e1892d0 fix a little 2016-01-15 23:35:41 +09:00
Rokt33r
b6b526dd57 activity record bug fix 2016-01-15 15:41:37 +09:00
Rokt33r
3ef7f19ffc devtools will automatically removed in production 2016-01-15 15:41:18 +09:00
Rokt33r
9d0d851c2e markdown strike bug fixed, no emoji shortcut, checkbox syntax added 2016-01-13 13:10:46 +09:00
Rokt33r
adb35b5bef clean up package 2016-01-10 06:46:20 +09:00
Rokt33r
acead09377 dull drag catch 2016-01-10 04:40:12 +09:00
Rokt33r
714cf43f6a set Basefont 2016-01-10 04:32:20 +09:00
Rokt33r
5df0755252 enhance applying config 2016-01-10 04:25:00 +09:00
Rokt33r
c14827b234 open finder => toggle finder 2016-01-10 03:31:13 +09:00
Rokt33r
ff9ef2af41 dataStore bug fix 2016-01-10 03:30:41 +09:00
Rokt33r
91ef5edcc3 load katex on Finder 2016-01-10 00:47:02 +09:00
Dick Choi
27302c6fcc hide devtool menu & fix default hotkey 2016-01-10 00:38:26 +09:00
Dick Choi
4d975da176 fix initialize bug & tutorial modal 2016-01-10 00:32:18 +09:00
Rokt33r
5b58d8a1e8 bump up to v0.5.1 2016-01-09 23:52:14 +09:00
Dick Choi
3105958afb enhance Editor UX again 2016-01-09 23:17:33 +09:00
Dick Choi
a505227d01 enhance editor UX 2016-01-09 23:03:23 +09:00
Dick Choi
673503b76f cmd+enter to confirm modal 2016-01-09 23:02:50 +09:00
Dick Choi
384682421d change font color of folder's article count when active 2016-01-09 22:26:45 +09:00
Dick Choi
2ddd6e6321 fix lineAnchor style 2016-01-09 22:23:49 +09:00
Dick Choi
86739aa1ac add tooltip for windows 2016-01-09 22:05:36 +09:00
Dick Choi
45a46cbc7a make configurable --disable-direct-write flag 2016-01-09 21:48:21 +09:00
Dick Choi
567f453232 add Fallback font 2016-01-09 21:02:41 +09:00
Dick Choi
890f654971 remove unnecessary codes 2016-01-09 21:02:30 +09:00
Dick Choi
572a0ac266 Hotkey for Windows 2016-01-09 20:36:27 +09:00
Dick Choi
d26ffdbe1a fix click bug of ModeSelect 2016-01-09 20:02:12 +09:00
Rokt33r
0bfc9236ed update overlay tutorial 2016-01-08 21:01:46 +09:00
Rokt33r
32e6394b3f search filter is now insensitive case, fix minor bugs 2016-01-08 19:12:37 +09:00
Rokt33r
09735b7f47 embed hotkey, config to preferences modal & fix datasaving sequence(Async, Queue) 2016-01-08 14:38:29 +09:00
Rokt33r
ee280d5c7b minor style change(move new post button left a little) 2016-01-07 09:41:05 +09:00
Rokt33r
c1b56e4cb6 add href, id, name transform for secure navigation 2016-01-07 09:14:30 +09:00
Rokt33r
6698d15f20 auto scrolling for markdown preview 2016-01-07 08:20:02 +09:00
Rokt33r
ef35fd02e5 add sup, sub 2016-01-07 08:19:31 +09:00
Rokt33r
8e70e20f9e hide save button 2016-01-06 20:33:13 +09:00
Rokt33r
9632bf5b93 bug in foldername sanitizer fixed 2016-01-06 04:42:48 +09:00
Rokt33r
dde0cab04b reset undo history 2016-01-06 04:41:42 +09:00
Rokt33r
c8337c7287 CommonMark 2016-01-06 04:41:19 +09:00
Rokt33r
15560a3bce render LaTeX 2016-01-05 04:53:40 +09:00
Rokt33r
2e3a60cf6e fix lineAnchor bug, preview tooltip must be shown only markdown mode 2016-01-05 02:15:24 +09:00
Rokt33r
08b0c43382 fix button tooltip & popup menu style 2016-01-04 19:57:01 +09:00
Rokt33r
4e0e11a611 add animation 2016-01-04 19:38:51 +09:00
Rokt33r
ef41dfca4c fix hover animation of anchor in md 2016-01-04 17:48:10 +09:00
Rokt33r
cfbca4b0fd list bug fix 2016-01-04 16:38:24 +09:00
Rokt33r
fdea9a68a1 One-click to edit 2016-01-04 01:45:45 +09:00
Rokt33r
47169e19aa Fix Preference style more 2016-01-04 01:43:02 +09:00
Rokt33r
0b03c8360b fix anchor style 2016-01-04 01:40:58 +09:00
Rokt33r
62f8af1455 escape double slash 2016-01-04 01:31:33 +09:00
Rokt33r
0934d452bb fix css code Preferences button, unsaved list in ArticleNav 2016-01-04 01:30:54 +09:00
Rokt33r
f2f31790b4 modeselect bug fix 2016-01-04 00:38:55 +09:00
Rokt33r
cf6ecc17cc fix wrong event name 2016-01-04 00:38:32 +09:00
Rokt33r
931f9bdce0 add uncache button to article detail & change share dropdown seems to be native 2016-01-02 22:39:26 +09:00
Rokt33r
bec0528a3a implement click to edit 2016-01-02 13:16:16 +09:00
Rokt33r
670f2b1fc3 add Click & Double click handler to MarkdownPreview 2016-01-02 13:15:50 +09:00
Rokt33r
f2f6de717b improve event handler for ModeSelect 2016-01-02 13:15:20 +09:00
Rokt33r
f8ad2eddf3 refactor CodeEditor 2016-01-02 13:14:46 +09:00
Rokt33r
c36a46cad6 fix style a little 2016-01-02 05:24:42 +09:00
Rokt33r
00360c77d2 add unsaved list & move new post button to top bar 2016-01-02 04:42:04 +09:00
Rokt33r
8a62cd386e add uncacheArticle, uncacheAllArticles action 2016-01-02 04:18:08 +09:00
Rokt33r
450327f093 remove unnecessary codes of webpack config 2016-01-02 04:17:08 +09:00
Rokt33r
e87ec04058 posting of ActivityRecord is available in production 2016-01-02 04:16:35 +09:00
Rokt33r
f9d41de8f1 Babel6 2015-12-30 14:19:01 +09:00
Rokt33r
f80a1a5f6b bump up version of ace editor 2015-12-29 03:09:20 +09:00
Rokt33r
f81caf962d focus back to list when preview mode 2015-12-29 02:55:00 +09:00
Rokt33r
d18fcf0a18 update grunt file 2015-12-29 02:54:24 +09:00
Rokt33r
0187217c86 renewal key binding 2015-12-29 02:18:37 +09:00
Rokt33r
b820bdec09 clean unnecessary default state of ArticleDetail 2015-12-28 21:36:27 +09:00
Rokt33r
adace2954e Hard-match for tags array 2015-12-28 21:35:52 +09:00
Rokt33r
6eeb8eeba6 realign tooltip of buttons in articleDetail 2015-12-28 21:02:11 +09:00
Rokt33r
dd2a8202ef bug fix 2015-12-28 21:01:43 +09:00
Rokt33r
d1cfd627bc add doublick handler for MarkdownPreview to turn off preview mode 2015-12-28 19:09:52 +09:00
Rokt33r
fb97b7443d show editor first if markdown article has empty content 2015-12-28 18:32:25 +09:00
Rokt33r
48fcd45d7d debug on saving data & error throw if dataStore get wrong data 2015-12-28 18:29:48 +09:00
Rokt33r
5cfc418d77 clean up main/*.styl 2015-12-28 18:29:11 +09:00
Rokt33r
f3fbe38247 add DeleteArticleModal 2015-12-28 18:14:05 +09:00
Rokt33r
a0a1c84db1 message for empty content like placeholder 2015-12-28 17:41:42 +09:00
Rokt33r
54d563f49e improve selectable area 2015-12-28 17:23:29 +09:00
Rokt33r
e8ee8b8a16 add toggle preview mode button 2015-12-28 17:20:26 +09:00
Rokt33r
c6ac44ba14 enhance style of topbar 2015-12-28 16:24:14 +09:00
Rokt33r
e4d8438801 save ALL 2015-12-28 16:11:42 +09:00
Rokt33r
f9539ab50a add only unsaved filter 2015-12-28 12:36:43 +09:00
Rokt33r
59f83c2432 lighten text color of content of ArticleList item 2015-12-28 12:01:06 +09:00
Rokt33r
cd789136c0 update ArticleList style 2015-12-28 01:32:30 +09:00
Rokt33r
54b5bc441e fix typo 2015-12-28 01:32:03 +09:00
Rokt33r
2537b6ba09 No edit mode 2015-12-28 00:59:36 +09:00
Rokt33r
013a1b4f51 cleanup style and class name of ArticleDetail 2015-12-26 23:07:04 +09:00
Rokt33r
d2377bd7c3 finder index will be reset after re-focusing 2015-12-25 18:38:14 +09:00
Rokt33r
c17314125e update package.json(bump up version of dependencies 2015-12-25 18:37:44 +09:00
Rokt33r
09a59480f3 add readme and update keywords in package.json 2015-12-25 12:19:16 +09:00
Rokt33r
63cc2ce70a use os default font 2015-12-25 11:35:34 +09:00
Rokt33r
4642e050ba apply font to textarea 2015-12-25 11:31:14 +09:00
Rokt33r
27a442ed2e fix path of module & contactform 2015-12-25 11:06:00 +09:00
Rokt33r
325ae00eeb refactor file structure 2015-12-25 05:41:10 +09:00
Rokt33r
152e4129b2 cleanup webpack config 2015-12-25 04:58:39 +09:00
Rokt33r
2ddcf84625 cleanup code of gruntfile 2015-12-25 04:55:38 +09:00
Rokt33r
13314700cd update gitignore 2015-12-25 04:44:14 +09:00
Dick Choi
a7a499a2b1 fix authenticode 2015-12-25 04:27:36 +09:00
Rokt33r
b646313b58 bump up version to v0.5.0 2015-12-25 04:27:36 +09:00
Rokt33r
f3ce4ca803 no zip for windows & must compile before release app 2015-12-25 04:27:36 +09:00
Rokt33r
93d99c0c47 fix wrong calling of updater 2015-12-25 04:27:36 +09:00
Dick Choi
ae1fc7572a add quit option to menu & fix sudden app quit caused by finder-window 2015-12-25 04:27:36 +09:00
Dick Choi
1a527cca10 set appdmg package optional 2015-12-25 04:27:36 +09:00
Rokt33r
c625513924 Add tooltip to editmode buttons 2015-12-25 04:27:35 +09:00
Rokt33r
3f58302a14 move intialize code from 'did-finish-load' 2015-12-25 04:27:35 +09:00
Rokt33r
63b199c9c2 scroll if trying to navigate up to out of screen 2015-12-25 04:27:35 +09:00
Rokt33r
fc64c565db cleanup codes 2015-12-25 04:27:35 +09:00
Rokt33r
91e60fa82b fix IPC bug: spawn 2 finder processes 2015-12-25 04:27:35 +09:00
Rokt33r
0cc52c2206 CommandをWindowsの場合Controlで表示する 2015-12-25 04:27:35 +09:00
Rokt33r
2ffe4ba70b clean up code 2015-12-25 04:27:35 +09:00
Rokt33r
2afd7e3687 Grunt deploy ready!!! 2015-12-25 04:24:19 +09:00
Rokt33r
a0f8d13c4f modify menu label(Boost-> Boostnote) 2015-12-21 22:41:16 +09:00
Rokt33r
2571ea021a focus previous app after hiding Finder window 2015-12-21 22:18:37 +09:00
Rokt33r
6950e05b6a set packaging for osx 2015-12-21 22:17:47 +09:00
Rokt33r
7eb767a268 IPC setup for windows & remove old updater code 2015-12-21 20:15:43 +09:00
Rokt33r
8e64abc4bc cleanup code 2015-12-20 15:47:29 +09:00
Dick Choi
52df793a74 remove remoteReleases URL 2015-12-19 14:52:29 +09:00
Dick Choi
8e44a421a2 switch npm scripts with Grunt task 2015-12-19 14:51:12 +09:00
Dick Choi
7f4ccdcac8 edit HTML title and loading picture URL 2015-12-19 13:59:24 +09:00
Dick Choi
03e8de2f62 Debug auto_update code of Windows app 2015-12-19 13:58:53 +09:00
Dick Choi
8b04eecc90 Add resources and use original logo 2015-12-19 00:55:12 +09:00
Dick Choi
16bcd86792 Refactor main process and add exception handler for Socket server 2015-12-19 00:54:11 +09:00
Dick Choi
be3c519a57 add silent option to notification 2015-12-17 11:34:50 +09:00
Dick Choi
8776cb1cea Finder behaviour for windows 2015-12-17 11:05:38 +09:00
Dick Choi
4c94503f9a variable name changed(WEB_URL -> SERVER_URL) 2015-12-16 09:18:47 +09:00
Dick Choi
48f57376d3 fix menu-template 2015-12-16 09:15:52 +09:00
Dick Choi
958469f526 set Notification icon for Windows 2015-12-15 14:00:56 +09:00
Dick Choi
2a774a7bb6 handle ctrl key as meta key when using Windows 2015-12-15 13:44:49 +09:00
Dick Choi
a872ad9d8b change IPC module(Raw buffer -> Socket) & set window behaviour for Windows 2015-12-15 13:43:28 +09:00
Dick Choi
2499a05473 Merge remote-tracking branch 'origin/master' into windows
Conflicts:
	browser/main/HomePage.js
	browser/main/HomePage/ArticleNavigator.js
	webpack.config.js
2015-12-15 13:06:01 +09:00
Rokt33r
6b66893ea4 Merge branch 'dev'
* dev:
  fix typo shareWith -> shareVia
  bump up version to 0.4.6
  Finderを開き直したら内容初期化
  ARTICLE_SHARE イベント追跡
  debug - 新規投稿が不可能
  enable copy (finder)
  modify dock.menu
  Folderの位置修正の保存
  add hot key:Navigate up(Ctrl + P) for CodeEditor
  switch API URL
  submit user name
  Url share done

Conflicts:
	package.json
2015-12-14 10:39:53 +09:00
Rokt33r
529c27aed5 fix typo shareWith -> shareVia 2015-12-13 22:46:59 +09:00
Rokt33r
70fc0afbc4 bump up version to 0.4.6 2015-12-13 19:45:03 +09:00
Rokt33r
09f81fd0d6 Finderを開き直したら内容初期化 2015-12-13 19:44:40 +09:00
Rokt33r
af7f2d4d5e ARTICLE_SHARE イベント追跡 2015-12-13 19:28:53 +09:00
Rokt33r
3bd5d6b9f6 debug - 新規投稿が不可能 2015-12-13 19:10:05 +09:00
Rokt33r
57912b5a5a enable copy (finder) 2015-12-13 18:36:31 +09:00
Rokt33r
a05f5b9737 modify dock.menu 2015-12-13 18:25:52 +09:00
Rokt33r
1963b586ac Folderの位置修正の保存 2015-12-13 17:01:36 +09:00
Rokt33r
3b9ad59849 add hot key:Navigate up(Ctrl + P) for CodeEditor 2015-12-13 16:29:45 +09:00
Rokt33r
79e0e5668d switch API URL 2015-12-13 16:29:01 +09:00
Rokt33r
0e8edf0c72 submit user name 2015-12-13 14:25:50 +09:00
Rokt33r
24e2544544 Url share done 2015-12-13 14:22:45 +09:00
Rokt33r
f3732c76ea Merge branch 'dev'
* dev:
  temporary setup
  0.4.5
  emit empty event(to create new record of today if not exists)
  MAIN_DETAIL_COPY, *_BY_SYNTAX, CLIENT_VERSION 追加
  debug ModeSelect component
  fix markdown style a little more
  Markdown styleを少し改善
  bump up version to 0.4.5 and change codesign path
  自動的にスクロールを合わせてくれる
  fix release path
  add electron-builder & modify deploy scripts
  bump up Ace editor
  Github release

Conflicts:
	package.json
2015-12-08 18:53:26 +09:00
Rokt33r
a4c72a9a86 temporary setup 2015-12-08 18:52:40 +09:00
Rokt33r
455610e586 0.4.5 2015-12-08 18:44:59 +09:00
Rokt33r
634d58b3ca emit empty event(to create new record of today if not exists) 2015-12-08 00:28:08 +09:00
Rokt33r
27bbd77e8c MAIN_DETAIL_COPY, *_BY_SYNTAX, CLIENT_VERSION 追加 2015-12-07 17:52:07 +09:00
Rokt33r
d8ae77ded7 debug ModeSelect component 2015-12-07 16:32:47 +09:00
Rokt33r
0648c04728 fix markdown style a little more 2015-12-05 19:16:04 +09:00
Rokt33r
57c26e3b4a Markdown styleを少し改善 2015-12-05 19:12:56 +09:00
Rokt33r
b03afff994 bump up version to 0.4.5 and change codesign path 2015-12-05 06:04:31 +09:00
Rokt33r
77f9e60177 自動的にスクロールを合わせてくれる 2015-12-05 05:56:53 +09:00
Rokt33r
35bb792496 fix release path 2015-12-04 08:59:17 +09:00
Rokt33r
8a87304800 add electron-builder & modify deploy scripts 2015-12-04 08:58:46 +09:00
Rokt33r
64bbe053f8 bump up Ace editor 2015-12-04 08:03:29 +09:00
Rokt33r
d3f420bf6d Github release 2015-12-04 08:03:17 +09:00
Rokt33r
7fcaaa297a Merge branch 'dev'
* dev:
  alert fix
  debug missing argument
  bump version
  rollback: setVisibleOnAllWorkspaces(true)
  FinderのActivity logをちゃんと取ってくる
  FinderのInputにLato fontが使われていない問題修正
  Search inputにRegExp operatorが入ると使えなかった問題改善
  User name change and modify style

Conflicts:
	package.json
2015-12-04 04:56:55 +09:00
Rokt33r
7c2d2044a9 alert fix 2015-12-04 04:56:04 +09:00
Rokt33r
aa32f59dc6 debug missing argument 2015-12-03 12:15:07 +09:00
Rokt33r
182af99e7c bump version 2015-12-03 12:02:29 +09:00
Rokt33r
5b520a7a81 rollback: setVisibleOnAllWorkspaces(true) 2015-12-03 12:02:21 +09:00
Rokt33r
364917c910 FinderのActivity logをちゃんと取ってくる 2015-12-03 07:59:47 +09:00
Rokt33r
ca7b9c786a FinderのInputにLato fontが使われていない問題修正 2015-12-03 07:25:35 +09:00
Rokt33r
15c2363098 Search inputにRegExp operatorが入ると使えなかった問題改善 2015-12-03 06:44:52 +09:00
Rokt33r
1a11095121 User name change and modify style 2015-12-03 05:32:10 +09:00
Rokt33r
2b384b1d15 fix updater bug 2015-12-01 02:00:18 +09:00
Rokt33r
a1d61edb9c Merge branch 'dev'
* dev:
  bump version
  Folder create modalを出したら、まっすぐName inputをFocusする
  編集警告が出ている時にCode editorがキー入力を認識する問題解決
  External link動きDebug
  add copy button
  External link用のDropdown menu追加
  コードを綺麗に
  Titleがなかったら灰色でUntitleと出す
  新規投稿 Cmd + n / Preview Cmd + P 追加
  articleのタイトルの基本タイトル追加 / 何も書かれていない時にUntitled labelをだす
  Finderのvisibile on all workspaces解除
  Searchbar tooltip changed(add exact match)
  change tray menu label(Open Finder => Open FInder window)
  Main windowの visible on all worpspace解除

Conflicts:
	package.json
2015-12-01 00:10:15 +09:00
Rokt33r
96a8687896 bump version 2015-11-30 23:11:31 +09:00
Rokt33r
0448773682 Folder create modalを出したら、まっすぐName inputをFocusする 2015-11-30 16:28:14 +09:00
Rokt33r
57998ba727 編集警告が出ている時にCode editorがキー入力を認識する問題解決 2015-11-30 16:22:10 +09:00
Rokt33r
de83447cb3 External link動きDebug 2015-11-30 12:53:46 +09:00
Rokt33r
eba19468d5 add copy button 2015-11-30 12:53:21 +09:00
Rokt33r
65c78df671 External link用のDropdown menu追加 2015-11-30 11:14:16 +09:00
Rokt33r
a7096aa89f コードを綺麗に 2015-11-30 04:28:23 +09:00
Rokt33r
15a50ef452 Titleがなかったら灰色でUntitleと出す 2015-11-30 04:17:52 +09:00
Rokt33r
04036e5c87 新規投稿 Cmd + n / Preview Cmd + P 追加 2015-11-30 03:44:58 +09:00
Rokt33r
2bbb5ef74e articleのタイトルの基本タイトル追加 / 何も書かれていない時にUntitled labelをだす 2015-11-29 18:57:49 +09:00
Rokt33r
91eb7feb3c Finderのvisibile on all workspaces解除 2015-11-29 11:08:13 +09:00
Rokt33r
978d77142c Searchbar tooltip changed(add exact match) 2015-11-29 11:05:18 +09:00
Rokt33r
e36478b9ac modify method name (api changed as electron updated) 2015-11-25 10:49:06 +09:00
Rokt33r
e1fe4dd693 change to use HTTPS for checking update 2015-11-25 09:51:33 +09:00
Rokt33r
b1ee949b1c This is a release version 2015-11-25 09:40:07 +09:00
Rokt33r
a0e5f8e97e Merge commit '80a0c59f878d899fc21b72f08eb8afeb1970f9ba'
* commit '80a0c59f878d899fc21b72f08eb8afeb1970f9ba':
  make it as prerelease
  bump up version
  MarkdownのCodeblockの行間をひろげる 
  編集中キャンセルを押しても消える情報があれば警告をだす
  データ移転バグ修正
  最初以降からはUpdaterがエラーをださない。
  Stream EPIPEエラー解決、データはこれからJSON保存
  notification デバッグ
  intercept entry point
  using ipc but not working in production
  bump up electron version 0.34 -> 0.35.1
  MarkdownでEmojiが使える
  Markdown内のコードにSyntax highlightenをいれる

Conflicts:
	main.js
2015-11-25 09:08:13 +09:00
Rokt33r
e9cfb2c4ee change tray menu label(Open Finder => Open FInder window) 2015-11-25 08:59:43 +09:00
Rokt33r
190b6edfb1 Main windowの visible on all worpspace解除 2015-11-25 08:50:56 +09:00
Rokt33r
80a0c59f87 make it as prerelease 2015-11-25 08:01:57 +09:00
Rokt33r
823fdec705 bump up version 2015-11-25 07:56:39 +09:00
Rokt33r
fe87dcced7 MarkdownのCodeblockの行間をひろげる 2015-11-25 07:42:22 +09:00
Rokt33r
137eb44516 編集中キャンセルを押しても消える情報があれば警告をだす 2015-11-25 07:42:02 +09:00
Rokt33r
f60d957102 データ移転バグ修正 2015-11-25 07:39:32 +09:00
Rokt33r
8f0b04504f 最初以降からはUpdaterがエラーをださない。 2015-11-25 07:38:46 +09:00
Rokt33r
2c39d8b1c8 Stream EPIPEエラー解決、データはこれからJSON保存 2015-11-25 07:37:33 +09:00
Rokt33r
d4d1c32288 notification デバッグ 2015-11-24 06:17:49 +09:00
Rokt33r
e4f39d2b6a intercept entry point 2015-11-24 04:16:43 +09:00
Rokt33r
e5a2bfbcbd using ipc but not working in production 2015-11-24 02:54:45 +09:00
Rokt33r
de3b76b31d bump up electron version 0.34 -> 0.35.1 2015-11-23 11:38:35 +09:00
Rokt33r
53455496bf MarkdownでEmojiが使える 2015-11-23 11:04:43 +09:00
Rokt33r
cc2a2f6dfb Markdown内のコードにSyntax highlightenをいれる 2015-11-23 10:39:21 +09:00
Rokt33r
ee4ac7371c Merge branch 'dev'
* dev:
  No node-notifier
  fix: 新しい記事を書く時に発生するバグ一体
  cleanup notification code
  Default文書修正
  開発中のものはデータを送らない
  初期記事内容修正cmd -> ctrl
  show devtool only devmode

Conflicts:
	main.js
2015-11-22 16:31:48 +09:00
Rokt33r
d5265407b9 No node-notifier 2015-11-22 15:57:44 +09:00
Rokt33r
954b3e9fc5 fix: 新しい記事を書く時に発生するバグ一体 2015-11-22 15:03:48 +09:00
Rokt33r
7d9894bef7 cleanup notification code 2015-11-21 22:07:59 +09:00
Rokt33r
3b34698e8b Default文書修正 2015-11-21 16:03:20 +09:00
Rokt33r
263cb581c4 開発中のものはデータを送らない 2015-11-21 06:37:23 +09:00
Rokt33r
1c9cb4516c 初期記事内容修正cmd -> ctrl 2015-11-21 06:36:50 +09:00
Rokt33r
ac4ceccb4f show devtool only devmode 2015-11-21 06:35:27 +09:00
Dick Choi
1318abd37e Windowsに合わせてUI修正(Font/Username/Key input:Command key -> Control key) 2015-11-17 09:01:25 +09:00
Dick Choi
76a031a8c9 Build環境設定 2015-11-17 09:00:08 +09:00
268 changed files with 10523 additions and 8648 deletions

View File

@@ -1,20 +1,8 @@
{ {
"stage": 0, "presets": ["react", "es2015"],
"env": { "env": {
"development": { "development": {
"plugins": ["react-transform"], "presets": ["react-hmre"]
"extra": {
"react-transform": {
"transforms": [{
"transform": "react-transform-hmr",
"imports": ["react"],
"locals": ["module"]
}, {
"transform": "react-transform-catch-errors",
"imports": ["react", "redbox-react"]
}]
}
}
} }
} }
} }

10
.gitignore vendored
View File

@@ -1,6 +1,10 @@
.DS_Store
.env .env
Desktop.ini
Thumbs.db
node_modules/* node_modules/*
!node_modules/boost !node_modules/boost
Boost-darwin-x64/ /dist
backup/ /compiled
compiled /secret
*.log

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "submodules/ace"]
path = submodules/ace
url = https://github.com/ajaxorg/ace-builds.git
branch = master

16
LICENSE Normal file
View File

@@ -0,0 +1,16 @@
Boostnote - the simplest note app
Copyright (C) 2016 MAISIN&CO.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

11
appdmg.json Normal file
View File

@@ -0,0 +1,11 @@
{
"title": "Boostnote",
"icon": "resources/dmg.icns",
"background": "resources/boostnote-install.png",
"icon-size": 80,
"contents": [
{ "x": 448, "y": 344, "type": "link", "path": "/Applications" },
{ "x": 192, "y": 344, "type": "file", "path": "dist/Boostnote-darwin-x64/Boostnote.app" }
]
}

View File

@@ -1,29 +0,0 @@
var BrowserWindow = require('browser-window')
var path = require('path')
var finderWindow = new BrowserWindow({
width: 640,
height: 400,
show: false,
frame: false,
resizable: false,
'zoom-factor': 1.0,
'always-on-top': true,
'web-preferences': {
'overlay-scrollbars': true,
'skip-taskbar': true
},
'standard-window': false
})
var url = path.resolve(__dirname, '../browser/finder/index.html')
finderWindow.loadUrl('file://' + url)
finderWindow.on('blur', function () {
finderWindow.hide()
})
finderWindow.setVisibleOnAllWorkspaces(true)
module.exports = finderWindow

View File

@@ -1,24 +0,0 @@
var BrowserWindow = require('browser-window')
var path = require('path')
var mainWindow = new BrowserWindow({
width: 1080,
height: 720,
'zoom-factor': 1.0,
'web-preferences': {
'overlay-scrollbars': true
},
'standard-window': false
})
var url = path.resolve(__dirname, '../browser/main/index.html')
mainWindow.loadUrl('file://' + url)
mainWindow.setVisibleOnAllWorkspaces(true)
mainWindow.webContents.on('new-window', function (e) {
e.preventDefault()
})
module.exports = mainWindow

View File

@@ -1,121 +0,0 @@
var BrowserWindow = require('browser-window')
module.exports = [
{
label: 'Electron',
submenu: [
{
label: 'About Boost',
selector: 'orderFrontStandardAboutPanel:'
},
{
type: 'separator'
},
{
label: 'Services',
submenu: []
},
{
type: 'separator'
},
{
label: 'Hide Boost',
accelerator: 'Command+H',
selector: 'hide:'
},
{
label: 'Hide Others',
accelerator: 'Command+Shift+H',
selector: 'hideOtherApplications:'
},
{
label: 'Show All',
selector: 'unhideAllApplications:'
},
{
type: 'separator'
},
{
label: 'Quit',
accelerator: 'Command+Q',
selector: 'terminate:'
}
]
},
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'Command+Z',
selector: 'undo:'
},
{
label: 'Redo',
accelerator: 'Shift+Command+Z',
selector: 'redo:'
},
{
type: 'separator'
},
{
label: 'Cut',
accelerator: 'Command+X',
selector: 'cut:'
},
{
label: 'Copy',
accelerator: 'Command+C',
selector: 'copy:'
},
{
label: 'Paste',
accelerator: 'Command+V',
selector: 'paste:'
},
{
label: 'Select All',
accelerator: 'Command+A',
selector: 'selectAll:'
}
]
},
{
label: 'View',
submenu: [
{
label: 'Reload',
accelerator: 'Command+R',
click: function () {
BrowserWindow.getFocusedWindow().reload()
}
}
]
},
{
label: 'Window',
submenu: [
{
label: 'Minimize',
accelerator: 'Command+M',
selector: 'performMiniaturize:'
},
{
label: 'Close',
accelerator: 'Command+W',
selector: 'performClose:'
},
{
type: 'separator'
},
{
label: 'Bring All to Front',
selector: 'arrangeInFront:'
}
]
},
{
label: 'Help',
submenu: []
}
]

View File

@@ -1,42 +0,0 @@
var autoUpdater = require('auto-updater')
var nn = require('node-notifier')
var app = require('app')
var path = require('path')
var version = app.getVersion()
var versionText = (version == null || version.length === 0) ? 'DEV version' : 'v' + version
var versionNotified = false
autoUpdater
.on('error', function (err, message) {
console.error(err)
console.error(message)
console.log(path.resolve(__dirname, '../resources/favicon-230x230.png'))
nn.notify({
title: 'Error! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: message
})
})
// .on('checking-for-update', function () {
// // Connecting
// console.log('checking...')
// })
.on('update-available', function () {
nn.notify({
title: 'Update is available!! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: 'Download started.. wait for the update ready.'
})
})
.on('update-not-available', function () {
if (!versionNotified) {
nn.notify({
title: 'Latest Build!! ' + versionText,
icon: path.resolve(__dirname, '../resources/favicon-230x230.png'),
message: 'Hope you to enjoy our app :D'
})
versionNotified = true
}
})
module.exports = autoUpdater

View File

@@ -0,0 +1,261 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
const ace = window.ace
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.changeHandler = (e) => this.handleChange(e)
this.blurHandler = (e) => {
e.stopPropagation()
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.root) {
isStillFocused = true
break
}
el = el.parentNode
}
if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e)
}
this.killedBuffer = ''
this.execHandler = (e) => {
console.info('ACE COMMAND >> %s', e.command.name)
switch (e.command.name) {
case 'gotolinestart':
e.preventDefault()
{
let position = this.editor.getCursorPosition()
this.editor.navigateTo(position.row, 0)
}
break
case 'gotolineend':
e.preventDefault()
let position = this.editor.getCursorPosition()
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
break
case 'jumptomatching':
e.preventDefault()
this.editor.navigateUp()
break
case 'removetolineend':
e.preventDefault()
let range = this.editor.getSelectionRange()
let session = this.editor.getSession()
if (range.isEmpty()) {
range.setEnd(range.start.row, session.getLine(range.start.row).length)
this.killedBuffer = session.getTextRange(range)
if (this.killedBuffer.length > 0) {
console.log('remove to lineend')
session.remove(range)
} else {
if (session.getLength() === range.start.row) {
return
}
range.setStart(range.start.row, range.end.col)
range.setEnd(range.start.row + 1, 0)
this.killedBuffer = '\n'
session.remove(range)
}
} else {
this.killedBuffer = session.getTextRange(range)
session.remove(range)
}
}
}
this.afterExecHandler = (e) => {
switch (e.command.name) {
case 'find':
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => {
el.removeEventListener('blur', this.blurHandler)
el.addEventListener('blur', this.blurHandler)
})
break
}
}
this.state = {
}
this.silentChange = false
}
componentWillReceiveProps (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
}
}
componentDidMount () {
let { mode, value, theme, fontSize } = this.props
this.value = value
let el = ReactDOM.findDOMNode(this)
let editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/' + theme)
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(fontSize)
editor.on('blur', this.blurHandler)
editor.commands.addCommand({
name: 'Emacs cursor up',
bindKey: {mac: 'Ctrl-P'},
exec: function (editor) {
editor.navigateUp(1)
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
},
readOnly: true
})
editor.commands.addCommand({
name: 'Emacs kill buffer',
bindKey: {mac: 'Ctrl-Y'},
exec: function (editor) {
editor.insert(this.killedBuffer)
}.bind(this),
readOnly: true
})
editor.commands.on('exec', this.execHandler)
editor.commands.on('afterExec', this.afterExecHandler)
var session = editor.getSession()
mode = _.find(modes, {name: mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(this.props.indentType === 'space')
session.setTabSize(this.props.indentSize)
session.setOption('useWorker', true)
session.setUseWrapMode(true)
session.setValue(_.isString(value) ? value : '')
session.on('change', this.changeHandler)
}
componentWillUnmount () {
this.editor.getSession().removeListener('change', this.changeHandler)
this.editor.removeListener('blur', this.blurHandler)
this.editor.commands.removeListener('exec', this.execHandler)
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
}
componentDidUpdate (prevProps, prevState) {
let { value } = this.props
this.value = value
let editor = this.editor
let session = this.editor.getSession()
if (prevProps.mode !== this.props.mode) {
let mode = _.find(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
if (prevProps.theme !== this.props.theme) {
editor.setTheme('ace/theme/' + this.props.theme)
}
if (prevProps.fontSize !== this.props.fontSize) {
editor.setFontSize(this.props.fontSize)
}
if (prevProps.indentSize !== this.props.indentSize) {
session.setTabSize(this.props.indentSize)
}
if (prevProps.indentType !== this.props.indentType) {
session.setUseSoftTabs(this.props.indentType === 'space')
}
}
handleChange (e) {
if (this.props.onChange) {
this.value = this.editor.getValue()
this.props.onChange(e)
}
}
getFirstVisibleRow () {
return this.editor.getFirstVisibleRow()
}
getCursorPosition () {
return this.editor.getCursorPosition()
}
moveCursorTo (row, col) {
this.editor.moveCursorTo(row, col)
}
scrollToLine (num) {
this.editor.scrollToLine(num, false, false)
}
focus () {
this.editor.focus()
}
blur () {
this.editor.blur()
}
reload () {
let session = this.editor.getSession()
session.removeListener('change', this.changeHandler)
session.setValue(this.props.value)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
render () {
let { className, fontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return (
<div
className={className == null
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root'
tabIndex='-1'
style={{
fontFamily: fontFamily.join(', ')
}}
/>
)
}
}
CodeEditor.propTypes = {
value: PropTypes.string,
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
readOnly: PropTypes.bool
}
CodeEditor.defaultProps = {
readOnly: false,
theme: 'xcode',
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
}
export default CodeEditor

View File

@@ -0,0 +1,151 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
class MarkdownEditor extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'PREVIEW'
}
}
componentDidMount () {
this.value = this.refs.code.value
}
componentDidUpdate () {
this.value = this.refs.code.value
}
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
}
handleContextMenu (e) {
let { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
let newStatus = this.state.status === 'PREVIEW'
? 'CODE'
: 'PREVIEW'
this.setState({
status: newStatus
}, () => {
if (newStatus === 'CODE') {
this.refs.code.focus()
} else {
this.refs.code.blur()
this.refs.preview.focus()
}
})
}
}
handleBlur (e) {
let { config } = this.props
if (config.editor.switchPreview === 'BLUR') {
let cursorPosition = this.refs.code.getCursorPosition()
this.setState({
status: 'PREVIEW'
}, () => {
this.refs.preview.focus()
this.refs.preview.scrollTo(cursorPosition.row)
})
}
}
handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date()
}
handlePreviewMouseUp (e) {
let { config } = this.props
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
}
}
focus () {
if (this.state.status === 'PREVIEW') {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
} else {
this.refs.code.focus()
}
}
reload () {
this.refs.code.reload()
}
render () {
let { className, value, config } = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
let previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
return (
<div className={className == null
? 'MarkdownEditor'
: `MarkdownEditor ${className}`
}
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='-1'
>
<CodeEditor styleName='codeEditor'
ref='code'
mode='markdown'
value={value}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
: 'preview--hide'
}
style={previewStyle}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='0'
value={value}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
/>
</div>
)
}
}
MarkdownEditor.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownEditor, styles)

View File

@@ -0,0 +1,23 @@
.root
position relative
.codeEditor
absolute top bottom left right
.codeEditor--hide
@extend .codeEditor
.preview
display block
absolute top bottom left right
z-index 100
background-color white
height 100%
width 100%
.preview--hide
@extend .preview
z-index 0
opacity 0
pointer-events none

View File

@@ -0,0 +1,163 @@
import React, { PropTypes } from 'react'
import markdown from 'browser/lib/markdown'
import _ from 'lodash'
import hljsTheme from 'browser/lib/hljsThemes'
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
const goExternal = function (e) {
e.preventDefault()
e.stopPropagation()
shell.openExternal(e.target.href)
}
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('\'Microsoft YaHei\'')
defaultFontFamily.unshift('meiryo')
}
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e)
}
handleContextMenu (e) {
this.props.onContextMenu(e)
}
handleMouseDown (e) {
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
}
handleMouseUp (e) {
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.rewriteIframe()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
}
componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value ||
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber
) this.rewriteIframe()
}
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', goExternal)
})
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? [fontFamily].concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode'
this.refs.root.contentWindow.document.head.innerHTML = `
<style>
@font-face {
font-family: 'Lato';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
${markdownStyle}
body {
font-family: ${fontFamily.join(', ')};
font-size: ${fontSize}px;
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
opacity: 0.5;
}
</style>
<link rel="stylesheet" href="../node_modules/highlight.js/styles/${codeBlockTheme}.css">
<link rel="stylesheet" href="../resources/katex.min.css">
`
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('mousedown', goExternal)
})
}
focus () {
this.refs.root.focus()
}
getWindow () {
return this.refs.root.contentWindow
}
scrollTo (targetRow) {
let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor')
for (let index = 0; index < lineAnchors.length; index++) {
let lineAnchor = lineAnchors[index]
let row = parseInt(lineAnchor.getAttribute('data-key'))
if (row > targetRow) {
let targetAnchor = lineAnchors[index - 1]
this.getWindow().scrollTo(0, targetAnchor.offsetTop)
break
}
}
}
render () {
let { className, style, tabIndex } = this.props
return (
<iframe className={className != null
? 'MarkdownPreview ' + className
: 'MarkdownPreview'
}
style={style}
tabIndex={tabIndex}
ref='root'
/>
)
}
}
MarkdownPreview.propTypes = {
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
value: PropTypes.string
}

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon' import ModeIcon from './ModeIcon'
import modes from 'boost/vars/modes' import modes from '../lib/modes'
import _ from 'lodash' import _ from 'lodash'
const IDLE_MODE = 'IDLE_MODE' const IDLE_MODE = 'IDLE_MODE'
@@ -22,7 +22,7 @@ export default class ModeSelect extends React.Component {
this.blurHandler = e => { this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search) let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) { if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
this.handleBlur() this.handleBlur(e)
} }
} }
window.addEventListener('click', this.blurHandler) window.addEventListener('click', this.blurHandler)
@@ -37,27 +37,9 @@ export default class ModeSelect extends React.Component {
} }
handleIdleSelectClick (e) { handleIdleSelectClick (e) {
this.setState({mode: EDIT_MODE}) this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
} ReactDOM.findDOMNode(this.refs.search).select()
})
componentDidUpdate (prevProps, prevState) {
if (prevState.mode !== this.state.mode && this.state.mode === EDIT_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
searchElement.focus()
if (this.searchKeyDownListener == null) {
this.searchKeyDownListener = e => this.handleSearchKeyDown
}
searchElement.addEventListener('keydown', this.searchKeyDownListener)
}
}
componentWillUpdate (nextProps, nextState) {
if (nextProps.mode !== this.state.mode && nextState.mode === IDLE_MODE) {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
} }
handleModeOptionClick (modeName) { handleModeOptionClick (modeName) {
@@ -82,9 +64,9 @@ export default class ModeSelect extends React.Component {
case 40: case 40:
e.preventDefault() e.preventDefault()
{ {
let search = _.escapeRegExp(this.state.search)
let filteredModes = modes let filteredModes = modes
.filter(mode => { .filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search) let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search)) let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched return nameMatched || aliasMatched
@@ -97,9 +79,9 @@ export default class ModeSelect extends React.Component {
case 13: case 13:
e.preventDefault() e.preventDefault()
{ {
let search = _.escapeRegExp(this.state.search)
let filteredModes = modes let filteredModes = modes
.filter(mode => { .filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search) let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search)) let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched return nameMatched || aliasMatched
@@ -107,19 +89,17 @@ export default class ModeSelect extends React.Component {
let targetMode = filteredModes[this.state.focusIndex] let targetMode = filteredModes[this.state.focusIndex]
if (targetMode != null) { if (targetMode != null) {
this.props.onChange(targetMode.name) this.props.onChange(targetMode.name)
this.handleBlur() this.setIdle()
} }
} }
break break
// esc // esc
case 27: case 27:
e.preventDefault()
e.stopPropagation()
this.handleBlur()
break
case 9: case 9:
this.handleBlur() e.stopPropagation()
this.setIdle()
} }
if (this.props.onKeyDown) this.props.onKeyDown(e)
} }
handleSearchChange (e) { handleSearchChange (e) {
@@ -129,15 +109,18 @@ export default class ModeSelect extends React.Component {
}) })
} }
handleBlur () { handleBlur (e) {
if (this.state.mode === EDIT_MODE) { if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
this.setState({ this.setIdle()
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
} }
if (this.props.onBlur != null) this.props.onBlur() }
setIdle () {
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
} }
render () { render () {
@@ -148,33 +131,32 @@ export default class ModeSelect extends React.Component {
if (this.state.mode === IDLE_MODE) { if (this.state.mode === IDLE_MODE) {
let mode = _.findWhere(modes, {name: this.props.value}) let mode = _.findWhere(modes, {name: this.props.value})
let modeName = mode != null ? mode.name : 'text' let modeName = mode != null ? mode.name : 'text'
let modeLabel = mode != null ? mode.label : 'Plain text' let modeLabel = mode != null ? mode.label : this.props.value
return ( return (
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}> <div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
<ModeIcon mode={modeName}/> <ModeIcon mode={modeName}/>{modeLabel}
<span className='modeLabel'>{modeLabel}</span>
</div> </div>
) )
} }
let search = _.escapeRegExp(this.state.search)
let filteredOptions = modes let filteredOptions = modes
.filter(mode => { .filter(mode => {
let search = this.state.search
let nameMatched = mode.name.match(search) let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search)) let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched return nameMatched || aliasMatched
}) })
.map((mode, index) => { .map((mode, index) => {
return ( return (
<div key={mode.name} className={index === this.state.focusIndex ? 'option active' : 'option'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div> <div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
) )
}) })
return ( return (
<div className={className + ' edit'}> <div className={className + ' edit'}>
<input onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/> <input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
<div ref='options' className='modeOptions hide'> <div ref='options' className='ModeSelect-options hide'>
{filteredOptions} {filteredOptions}
</div> </div>
</div> </div>
@@ -186,5 +168,5 @@ ModeSelect.propTypes = {
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
onBlur: PropTypes.func onKeyDown: PropTypes.func
} }

View File

@@ -0,0 +1,253 @@
global-reset()
borderColor = #D0D0D0 // using
highlightenBorderColor = darken(borderColor, 20%)
invBorderColor = #404849
brandBorderColor = #3FB399
focusBorderColor = #369DCD
buttonBorderColor = #4C4C4C
lightButtonColor = #898989
hoverBackgroundColor= transparentify(#444, 4%) // using
inactiveTextColor = #888 // using
textColor = #4D4D4D // using
backgroundColor= white
fontSize= 14px // using
shadowColor= #C5C5C5
invBackgroundColor = #4C4C4C
invTextColor = white
btnColor = #888
btnHighlightenColor = #000
brandColor = #2BAC8F
popupShadow = 0 0 5px 0 #888
tableHeadBgColor = white
tableOddBgColor = #F9F9F9
tableEvenBgColor = white
facebookColor= #3b5998
githubBtn= #201F1F
// using
successBackgroundColor= #E0F0D9
successTextColor= #3E753F
errorBackgroundColor= #F2DEDE
errorTextColor= #A64444
infoBackgroundColor= #D9EDF7
infoTextColor= #34708E
popupZIndex= 500
body
font-size 16px
padding 15px
font-family helvetica, arial, sans-serif
line-height 1.6
overflow-x hidden
user-select all
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space nowrap
text-indent 0
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
background-color errorBackgroundColor
color errorTextColor
padding 5px
margin -5px
border-radius 5px
div.math-rendered
text-align center
.math-failed
background-color alpha(red, 0.1)
color darken(red, 15%)
padding 5px
margin 5px 0
border-radius 5px
sup
position relative
top -.4em
font-size 0.8em
vertical-align top
sub
position relative
bottom -.4em
font-size 0.8em
vertical-align top
a
color brandColor
text-decoration none
padding 5px
border-radius 5px
margin -5px
transition .1s
display inline-block
img
vertical-align sub
&:hover
color lighten(brandColor, 5%)
text-decoration underline
background-color alpha(#FFC95C, 0.3)
&:visited
color brandColor
&.lineAnchor
padding 0
margin 0
display block
font-size 0
height 0
hr
border-top none
border-bottom solid 1px borderColor
margin 15px 0
h1, h2, h3, h4, h5, h6
font-weight bold
h1
font-size 2.25em
padding-bottom 0.3em
line-height 1.2em
border-bottom solid 1px borderColor
margin 1em 0 0.44em
&:first-child
margin-top 0
h2
font-size 1.75em
padding-bottom 0.3em
line-height 1.225em
border-bottom solid 1px borderColor
margin 1em 0 0.57em
&:first-child
margin-top 0
h3
font-size 1.5em
line-height 1.43em
margin 1em 0 0.66em
h4
font-size 1.25em
line-height 1.4em
margin 1em 0 0.8em
h5
font-size 1em
line-height 1.4em
margin 1em 0 1em
h6
font-size 1em
line-height 1.4em
margin 1em 0 1em
color #777
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
margin-top 1em
p
line-height 1.6em
margin 0 0 1em
white-space pre-line
img
max-width 100%
strong, b
font-weight bold
em, i
font-style italic
s, del, strike
text-decoration line-through
u
text-decoration underline
blockquote
border-left solid 4px brandBorderColor
margin 0 0 1em
padding 0 25px
ul
list-style-type disc
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
&>li>ul
list-style-type circle
&>li>ul
list-style-type square
ol
list-style-type decimal
padding-left 2em
margin-bottom 1em
li
display list-item
&>li>ul, &>li>ol
margin 0
code
padding 0.2em 0.4em
background-color #f7f7f7
border-radius 3px
font-size 0.85em
text-decoration none
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre
padding 0.5em !important
border solid 1px alpha(borderColor, 0.5)
border-radius 5px
overflow-x auto
margin 0 0 1em
line-height 1.35
code
margin 0
background-color inherit
padding 0
border none
border-radius 0
pre
border none
margin -5px
&>span.lineNumber
display none
float left
font-size 0.85em
margin 0 0.5em 0 -0.5em
border-right 1px solid
text-align right
&>span
display block
padding 0 .5em 0 1em
table
display block
width 100%
margin 0 0 1em
thead
tr
background-color tableHeadBgColor
th
border-style solid
padding 6px 13px
line-height 1.6
border-width 1px 0 2px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor
tbody
tr:nth-child(2n + 1)
background-color tableOddBgColor
tr:nth-child(2n)
background-color tableEvenBgColor
td
border-style solid
padding 6px 13px
line-height 1.6
border-width 0 0 1px 1px
border-color borderColor
&:last-child
border-right solid 1px borderColor

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import CodeEditor from 'boost/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'boost/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import ModeIcon from 'boost/components/ModeIcon' import ModeIcon from 'browser/components/ModeIcon'
export default class FinderDetail extends React.Component { export default class FinderDetail extends React.Component {
render () { render () {
@@ -24,7 +24,7 @@ export default class FinderDetail extends React.Component {
<div className='content'> <div className='content'>
{activeArticle.mode === 'markdown' {activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/> ? <MarkdownPreview content={activeArticle.content}/>
: <CodeEditor readOnly mode={activeArticle.mode} code={activeArticle.content}/> : <CodeEditor readOnly article={activeArticle}/>
} }
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon' import ModeIcon from 'browser/components/ModeIcon'
import { selectArticle } from './actions' import { selectArticle } from './actions'
export default class FinderList extends React.Component { export default class FinderList extends React.Component {

View File

@@ -16,11 +16,8 @@ export function searchArticle (input) {
} }
} }
export function refreshData () { export function refreshData (data) {
console.log('refreshing data') console.log('refreshing data')
let data = JSON.parse(localStorage.getItem('local'))
if (data == null) return null
let { folders, articles } = data let { folders, articles } = data
return { return {
@@ -31,3 +28,12 @@ export function refreshData () {
} }
} }
} }
export default {
SELECT_ARTICLE,
SEARCH_ARTICLE,
REFRESH_DATA,
selectArticle,
searchArticle,
refreshData
}

View File

@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Boost Finder</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="shortcut icon" href="favicon.ico">
<style>
@font-face {
font-family: 'Lato';
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../../resources/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
</style>
</head>
<body>
<div id="content"></div>
<script src="../../submodules/ace/src-min/ace.js"></script>
<script>
require('web-frame').setZoomLevelLimits(1, 1)
var scriptUrl = process.env.BOOST_ENV === 'development'
? 'http://localhost:8080/assets/finder.js'
: '../../compiled/finder.js'
var scriptEl=document.createElement('script')
scriptEl.setAttribute("type","text/javascript")
scriptEl.setAttribute("src", scriptUrl)
document.getElementsByTagName("head")[0].appendChild(scriptEl)
</script>
</body>
</html>

View File

@@ -6,21 +6,50 @@ import { createStore } from 'redux'
import FinderInput from './FinderInput' import FinderInput from './FinderInput'
import FinderList from './FinderList' import FinderList from './FinderList'
import FinderDetail from './FinderDetail' import FinderDetail from './FinderDetail'
import { selectArticle, searchArticle, refreshData } from './actions' import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash' import _ from 'lodash'
import activityRecord from 'boost/activityRecord' import dataStore from 'browser/lib/dataStore'
import fetchConfig from '../lib/fetchConfig'
const electron = require('electron')
const { clipboard, ipcRenderer, remote } = electron
const path = require('path')
import remote from 'remote' let config = fetchConfig()
var hideFinder = remote.getGlobal('hideFinder') applyConfig(config)
import clipboard from 'clipboard'
var notifier = require('node-notifier') ipcRenderer.on('config-apply', function (e, newConfig) {
var path = require('path') config = newConfig
function getIconPath () { applyConfig(config)
return path.resolve(global.__dirname, '../../resources/favicon-230x230.png') })
function applyConfig () {
let body = document.body
body.setAttribute('data-theme', config['theme-ui'])
let hljsCss = document.getElementById('hljs-css')
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
} }
require('../styles/finder/index.styl') if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
function hideFinder () {
ipcRenderer.send('hide-finder')
}
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
}
return new window.Notification(title, options)
}
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
const FOLDER_FILTER = 'FOLDER_FILTER' const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER' const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
@@ -33,11 +62,21 @@ class FinderMain extends React.Component {
} }
componentDidMount () { componentDidMount () {
this.keyDownHandler = e => this.handleKeyDown(e)
document.addEventListener('keydown', this.keyDownHandler)
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
this.focusHandler = e => {
let { dispatch } = this.props
dispatch(searchArticle(''))
dispatch(selectArticle(null))
}
window.addEventListener('focus', this.focusHandler)
} }
handleClick (e) { componentWillUnmount () {
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() document.removeEventListener('keydown', this.keyDownHandler)
window.removeEventListener('focus', this.focusHandler)
} }
handleKeyDown (e) { handleKeyDown (e) {
@@ -59,17 +98,21 @@ class FinderMain extends React.Component {
hideFinder() hideFinder()
e.preventDefault() e.preventDefault()
} }
if (e.keyCode === 91 || e.metaKey) {
return
}
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
} }
saveToClipboard () { saveToClipboard () {
let { activeArticle } = this.props let { activeArticle } = this.props
clipboard.writeText(activeArticle.content) clipboard.writeText(activeArticle.content)
activityRecord.emit('FINDER_COPY')
notifier.notify({ ipcRenderer.send('copy-finder')
icon: getIconPath(), notify('Saved to Clipboard!', {
'title': 'Saved to Clipboard!', body: 'Paste it wherever you want!',
'message': 'Paste it wherever you want!' silent: true
}) })
hideFinder() hideFinder()
} }
@@ -102,7 +145,7 @@ class FinderMain extends React.Component {
let { articles, activeArticle, status, dispatch } = this.props let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard() let saveToClipboard = () => this.saveToClipboard()
return ( return (
<div onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'> <div className='Finder'>
<FinderInput <FinderInput
handleSearchChange={e => this.handleSearchChange(e)} handleSearchChange={e => this.handleSearchChange(e)}
ref='finderInput' ref='finderInput'
@@ -156,6 +199,14 @@ function buildFilter (key) {
return {type: TEXT_FILTER, value: key} return {type: TEXT_FILTER, value: key}
} }
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
}
function remap (state) { function remap (state) {
let { articles, folders, status } = state let { articles, folders, status } = state
@@ -172,10 +223,10 @@ function remap (state) {
let targetFolders let targetFolders
if (folders != null) { if (folders != null) {
let exactTargetFolders = folders.filter(folder => { let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`))) return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
}) })
let fuzzyTargetFolders = folders.filter(folder => { let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`))) return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
}) })
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders) targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
@@ -188,7 +239,7 @@ function remap (state) {
if (textFilters.length > 0) { if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => { articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => { return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i')) return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
}) })
}, articles) }, articles)
} }
@@ -196,7 +247,7 @@ function remap (state) {
if (tagFilters.length > 0) { if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => { articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => { return articles.filter(article => {
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i'))) return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
}) })
}, articles) }, articles)
} }
@@ -205,7 +256,6 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey}) let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0] if (activeArticle == null) activeArticle = articles[0]
console.log(status.search)
return { return {
articles, articles,
activeArticle, activeArticle,
@@ -216,13 +266,19 @@ function remap (state) {
var Finder = connect(remap)(FinderMain) var Finder = connect(remap)(FinderMain)
var store = createStore(reducer) var store = createStore(reducer)
function refreshData () {
let data = dataStore.getData(true)
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => { window.onfocus = e => {
store.dispatch(refreshData()) refreshData()
activityRecord.emit('FINDER_OPEN')
} }
ReactDOM.render(( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
<Finder/> <Finder/>
</Provider> </Provider>
), document.getElementById('content')) ), document.getElementById('content'), function () {
refreshData()
})

View File

@@ -1,10 +1,8 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions' import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
let data = JSON.parse(localStorage.getItem('local')) let initialArticles = []
let initialFolders = []
let initialArticles = data != null ? data.articles : []
let initialFolders = data != null ? data.folders : []
let initialStatus = { let initialStatus = {
articleKey: null, articleKey: null,
search: '' search: ''

View File

@@ -0,0 +1,5 @@
import CSSModules from 'react-css-modules'
export default function (component, styles) {
return CSSModules(component, styles, {errorWhenNotFound: false})
}

View File

@@ -1,8 +1,11 @@
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import keygen from 'boost/keygen' import dataStore from './dataStore'
import dataStore from 'boost/dataStore' import { request, SERVER_URL } from './api'
import { request, WEB_URL } from 'boost/api' import clientKey from './clientKey'
const electron = require('electron')
const version = electron.remote.app.getVersion()
function isSameDate (a, b) { function isSameDate (a, b) {
a = moment(a).utcOffset(+540).format('YYYYMMDD') a = moment(a).utcOffset(+540).format('YYYYMMDD')
@@ -16,6 +19,7 @@ export function init () {
if (records == null) { if (records == null) {
saveAllRecords([]) saveAllRecords([])
} }
emit(null)
postRecords() postRecords()
if (window != null) { if (window != null) {
@@ -24,16 +28,6 @@ export function init () {
} }
} }
export function getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen()
localStorage.setItem('clientKey', clientKey)
}
return clientKey
}
export function getAllRecords () { export function getAllRecords () {
return JSON.parse(localStorage.getItem('activityRecords')) return JSON.parse(localStorage.getItem('activityRecords'))
} }
@@ -47,6 +41,11 @@ Post all records(except today)
and remove all posted records and remove all posted records
*/ */
export function postRecords (data) { export function postRecords (data) {
if (process.env.NODE_ENV !== 'production') {
console.log('post failed - NOT PRODUCTION ')
return
}
let records = getAllRecords() let records = getAllRecords()
records = records.filter(record => { records = records.filter(record => {
return !isSameDate(new Date(), record.date) return !isSameDate(new Date(), record.date)
@@ -59,10 +58,10 @@ export function postRecords (data) {
console.log('posting...', records) console.log('posting...', records)
let input = { let input = {
clientKey: getClientKey(), clientKey: clientKey.get(),
records records
} }
return request.post(WEB_URL + 'apis/activity') return request.post(SERVER_URL + 'apis/activity')
.send(input) .send(input)
.then(res => { .then(res => {
let records = getAllRecords() let records = getAllRecords()
@@ -77,7 +76,7 @@ export function postRecords (data) {
}) })
} }
export function emit (type, data) { export function emit (type, data = {}) {
let records = getAllRecords() let records = getAllRecords()
let index = _.findIndex(records, record => { let index = _.findIndex(records, record => {
@@ -90,7 +89,6 @@ export function emit (type, data) {
records.push(todayRecord) records.push(todayRecord)
} }
else todayRecord = records[index] else todayRecord = records[index]
console.log(type)
switch (type) { switch (type) {
case 'ARTICLE_CREATE': case 'ARTICLE_CREATE':
case 'ARTICLE_UPDATE': case 'ARTICLE_UPDATE':
@@ -100,6 +98,8 @@ export function emit (type, data) {
case 'FOLDER_DESTROY': case 'FOLDER_DESTROY':
case 'FINDER_OPEN': case 'FINDER_OPEN':
case 'FINDER_COPY': case 'FINDER_COPY':
case 'MAIN_DETAIL_COPY':
case 'ARTICLE_SHARE':
todayRecord[type] = todayRecord[type] == null todayRecord[type] = todayRecord[type] == null
? 1 ? 1
: todayRecord[type] + 1 : todayRecord[type] + 1
@@ -107,9 +107,26 @@ export function emit (type, data) {
break break
} }
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
let recordKey = type + '_BY_SYNTAX'
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
? 1
: todayRecord[recordKey][data.mode] + 1
}
let storeData = dataStore.getData() let storeData = dataStore.getData()
todayRecord.FOLDER_COUNT = _.isArray(storeData.folders) ? storeData.folders.length : 0 todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
todayRecord.ARTICLE_COUNT = _.isArray(storeData.articles) ? storeData.articles.length : 0 todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
todayRecord.CLIENT_VERSION = version
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
if (sum[article.mode] == null) sum[article.mode] = 1
else sum[article.mode]++
return sum
}, {}) : 0
saveAllRecords(records) saveAllRecords(records)
} }
@@ -117,6 +134,5 @@ export function emit (type, data) {
export default { export default {
init, init,
emit, emit,
getClientKey,
postRecords postRecords
} }

21
browser/lib/api.js Normal file
View File

@@ -0,0 +1,21 @@
import superagent from 'superagent'
import superagentPromise from 'superagent-promise'
export const SERVER_URL = 'https://b00st.io/'
// export const SERVER_URL = 'http://localhost:3333/'
export const request = superagentPromise(superagent, Promise)
export function shareViaPublicURL (input) {
return request
.post(SERVER_URL + 'apis/share')
// .set({
// Authorization: 'Bearer ' + auth.token()
// })
.send(input)
}
export default {
SERVER_URL,
shareViaPublicURL
}

23
browser/lib/clientKey.js Normal file
View File

@@ -0,0 +1,23 @@
import _ from 'lodash'
import keygen from './keygen'
function getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen(20)
setClientKey(clientKey)
}
return clientKey
}
function setClientKey (newKey) {
localStorage.setItem('clientKey', newKey)
}
getClientKey()
export default {
get: getClientKey,
set: setClientKey
}

22
browser/lib/consts.js Normal file
View File

@@ -0,0 +1,22 @@
const consts = {
FOLDER_COLORS: [
'#E10051',
'#FF8E00',
'#E8D252',
'#3FD941',
'#30D5C8',
'#2BA5F7',
'#B013A4'
],
FOLDER_COLOR_NAMES: [
'Razzmatazz (Red)',
'Pizazz (Orange)',
'Confetti (Yellow)',
'Emerald (Green)',
'Turquoise',
'Dodger Blue',
'Violet Eggplant'
]
}
module.exports = consts

156
browser/lib/dataStore.js Normal file
View File

@@ -0,0 +1,156 @@
import keygen from './keygen'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const path = require('path')
let defaultContent = 'Boost is a brand new note App for programmers.\n\n> 下に日本語版があります。\n\n# \u25CEfeature\n\nBoost has some preponderant functions for efficient engineer\'s task.See some part of it.\n\n1. classify information by\u300CFolders\u300D\n2. deal with great variety of syntax\n3. Finder function\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n# 1. classify information by \u300CFolders\u300D- access the information you needed easily.\n\n\u300CFolders\u300D which on the left side bar. Press plus button now. flexible way of classification.\n- Create Folder every language or flamework\n- Make Folder for your own casual memos\n\n# 2. Deal with a great variety of syntax \u2013 instead of your brain\nSave handy all information related with programming\n- Use markdown and gather api specification\n- Well using module and snippet\n\nSave them on Boost, you don\'t need to rewrite or re-search same code again.\n\n# 3. Load Finder function \u2013 now you don\'t need to spell command by hand typing.\n\n**Shift +ctrl+tab** press buttons at same time.\nThen, the window will show up for search Boost contents that instant.\n\nUsing cursor key to chose, press enter, cmd+v to paste and\u2026 please check it out by your own eye.\n\n- Such command spl or linux which programmers often use but troublesome to hand type\n\n- (Phrases commonly used for e-mail or customer support)\n\nWe support preponderant efficiency\n\n\uFF0A\u3000\uFF0A\u3000\uFF0A\u3000\uFF0A\n\n## \u25CEfor more information\nFrequently updated with this blog ( http:\/\/blog-jp.b00st.io )\n\nHave wonderful programmer life!\n\n## Hack your memory**\n\n\n\n# 日本語版\n\n**Boost**は全く新しいエンジニアライクのノートアプリです。\n\n# ◎特徴\nBoostはエンジニアの仕事を圧倒的に効率化するいくつかの機能を備えています。\nその一部をご紹介します。\n1. Folderで情報を分類\n2. 豊富なsyantaxに対応\n3. Finder機能\n\n\n   \n\n# 1. Folderで情報を分類、欲しい情報にすぐアクセス。\n左側のバーに存在する「Folders」。\n今すぐプラスボタンを押しましょう。\n分類の仕方も自由自在です。\n- 言語やフレームワークごとにFolderを作成\n- 自分用のカジュアルなメモをまとめる場としてFolderを作成\n\n\n# 2. 豊富なsyntaxに対応、自分の脳の代わりに。\nプログラミングに関する情報を全て、手軽に保存しましょう。\n- mdで、apiの仕様をまとめる\n- よく使うモジュールやスニペット\n\nBoostに保存しておくことで、何度も同じコードを書いたり調べたりする必要がなくなります。\n\n# 3. Finder機能を搭載、もうコマンドを手打ちする必要はありません。\n**「shift+ctrl+tab」** を同時に押してみてください。\nここでは、一瞬でBoostの中身を検索するウィンドウを表示させることができます。\n\n矢印キーで選択、Enterを押し、cmd+vでペーストすると…続きはご自身の目でお確かめください。\n- sqlやlinux等の、よく使うが手打ちが面倒なコマンド\n- (メールやカスタマーサポート等でよく使うフレーズ)\n\n私たちは、圧倒的な効率性を支援します。\n\   \n\n\n## ◎詳しくは\nこちらのブログ( http://blog-jp.b00st.io )にて随時更新しています。\n\nそれでは素晴らしいエンジニアライフを\n\n## Hack your memory'
let data = null
function getLocalPath () {
return path.join(remote.app.getPath('userData'), 'local.json')
}
function forgeInitialRepositories () {
let defaultRepo = {
key: keygen(),
name: 'local',
type: 'userData',
user: {
name: 'New user'
}
}
if (process.platform === 'darwin') {
defaultRepo.user.name = remote.process.env.USER
} else if (process.platform === 'win32') {
defaultRepo.user.name = remote.process.env.USERNAME
}
return [defaultRepo]
}
function getRepositories () {
let raw = localStorage.getItem('repositories')
try {
let parsed = JSON.parse(raw)
if (!_.isArray(parsed)) {
throw new Error('repositories data is corrupted. re-init data.')
}
return parsed
} catch (e) {
console.log(e)
let newRepos = forgeInitialRepositories()
saveRepositories(newRepos)
return newRepos
}
}
function saveRepositories (repos) {
localStorage.setItem('repositories', JSON.stringify(repos))
}
export function getUser (repoName) {
if (repoName == null) {
return getRepositories()[0]
}
return null
}
export function saveUser (repoName, user) {
let repos = getRepositories()
if (repoName == null) {
Object.assign(repos[0].user, user)
}
saveRepositories(repos)
}
export function init () {
// set repositories info
getRepositories()
data = jetpack.read(getLocalPath(), 'json')
if (data == null) {
let defaultFolder = {
name: 'default',
key: keygen()
}
let defaultArticle = {
title: 'About Boost',
tags: ['boost', 'intro'],
content: defaultContent,
mode: 'markdown',
key: keygen(),
FolderKey: defaultFolder.key,
createdAt: new Date(),
updatedAt: new Date()
}
data = {
articles: [defaultArticle],
folders: [defaultFolder],
version: '0.4'
}
saveData()
}
}
export function getData (forceRead) {
if (forceRead) {
try {
data = jetpack.read(getLocalPath(), 'json')
} catch (e) {}
}
return data
}
let timer = null
let isSaving = false
let saveAgain = false
function saveData () {
timer = null
isSaving = true
jetpack.writeAsync(getLocalPath(), data)
.then(function () {
isSaving = false
if (saveAgain) {
saveAgain = false
queueSave()
}
})
}
function queueSave () {
if (!isSaving) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(saveData, 500)
} else {
saveAgain = true
}
}
export function setArticles (articles) {
if (!_.isArray(articles)) throw new Error('Articles must be an array')
let data = getData()
data.articles = articles
queueSave()
}
export function setFolders (folders) {
if (!_.isArray(folders)) throw new Error('Folders must be an array')
let data = getData()
data.folders = folders
queueSave()
}
export default {
getUser,
saveUser,
init,
getData,
setArticles,
setFolders
}

78
browser/lib/hljsThemes.js Normal file
View File

@@ -0,0 +1,78 @@
const hljsThemeList = [
{caption: 'Default', name: 'default'},
{caption: 'Agate', name: 'agate'},
{caption: 'Androidstudio', name: 'androidstudio'},
{caption: 'Arduino Light', name: 'arduino-light'},
{caption: 'Arta', name: 'arta'},
{caption: 'Ascetic', name: 'ascetic'},
{caption: 'Atelier Cave Dark', name: 'atelier-cave-dark'},
{caption: 'Atelier Cave Light', name: 'atelier-cave-light'},
{caption: 'Atelier Dune Dark', name: 'atelier-dune-dark'},
{caption: 'Atelier Dune Light', name: 'atelier-dune-light'},
{caption: 'Atelier Estuary Dark', name: 'atelier-estuary-dark'},
{caption: 'Atelier Estuary Light', name: 'atelier-estuary-light'},
{caption: 'Atelier Forest Dark', name: 'atelier-forest-dark'},
{caption: 'Atelier Forest Light', name: 'atelier-forest-light'},
{caption: 'Atelier Heath Dark', name: 'atelier-heath-dark'},
{caption: 'Atelier Heath Light', name: 'atelier-heath-light'},
{caption: 'Atelier Lakeside Dark', name: 'atelier-lakeside-dark'},
{caption: 'Atelier Lakeside Light', name: 'atelier-lakeside-light'},
{caption: 'Atelier Plateau Dark', name: 'atelier-plateau-dark'},
{caption: 'Atelier Plateau Light', name: 'atelier-plateau-light'},
{caption: 'Atelier Savanna Dark', name: 'atelier-savanna-dark'},
{caption: 'Atelier Savanna Light', name: 'atelier-savanna-light'},
{caption: 'Atelier Seaside Dark', name: 'atelier-seaside-dark'},
{caption: 'Atelier Seaside Light', name: 'atelier-seaside-light'},
{caption: 'Atelier Sulphurpool Dark', name: 'atelier-sulphurpool-dark'},
{caption: 'Atelier Sulphurpool Light', name: 'atelier-sulphurpool-light'},
{caption: 'Brown Paper', name: 'brown-paper'},
{caption: 'Codepen Embed', name: 'codepen-embed'},
{caption: 'Color Brewer', name: 'color-brewer'},
{caption: 'Dark', name: 'dark'},
{caption: 'Darkula', name: 'darkula'},
{caption: 'Docco', name: 'docco'},
{caption: 'Dracula', name: 'dracula'},
{caption: 'Far', name: 'far'},
{caption: 'Foundation', name: 'foundation'},
{caption: 'Github Gist', name: 'github-gist'},
{caption: 'Github', name: 'github'},
{caption: 'Googlecode', name: 'googlecode'},
{caption: 'Grayscale', name: 'grayscale'},
{caption: 'Gruvbox Dark', name: 'gruvbox.dark'},
{caption: 'Gruvbox Light', name: 'gruvbox.light'},
{caption: 'Hopscotch', name: 'hopscotch'},
{caption: 'Hybrid', name: 'hybrid'},
{caption: 'Idea', name: 'idea'},
{caption: 'Ir Black', name: 'ir-black'},
{caption: 'Kimbie Dark', name: 'kimbie.dark'},
{caption: 'Kimbie Light', name: 'kimbie.light'},
{caption: 'Magula', name: 'magula'},
{caption: 'Mono Blue', name: 'mono-blue'},
{caption: 'Monokai Sublime', name: 'monokai-sublime'},
{caption: 'Monokai', name: 'monokai'},
{caption: 'Obsidian', name: 'obsidian'},
{caption: 'Paraiso Dark', name: 'paraiso-dark'},
{caption: 'Paraiso Light', name: 'paraiso-light'},
{caption: 'Pojoaque', name: 'pojoaque'},
{caption: 'Qtcreator Dark', name: 'qtcreator_dark'},
{caption: 'Qtcreator Light', name: 'qtcreator_light'},
{caption: 'Railscasts', name: 'railscasts'},
{caption: 'Rainbow', name: 'rainbow'},
{caption: 'School Book', name: 'school-book'},
{caption: 'Solarized Dark', name: 'solarized-dark'},
{caption: 'Solarized Light', name: 'solarized-light'},
{caption: 'Sunburst', name: 'sunburst'},
{caption: 'Tomorrow Night Blue', name: 'tomorrow-night-blue'},
{caption: 'Tomorrow Night Bright', name: 'tomorrow-night-bright'},
{caption: 'Tomorrow Night Eighties', name: 'tomorrow-night-eighties'},
{caption: 'Tomorrow Night', name: 'tomorrow-night'},
{caption: 'Tomorrow', name: 'tomorrow'},
{caption: 'Vs', name: 'vs'},
{caption: 'Xcode', name: 'xcode'},
{caption: 'Xt 256', name: 'xt256'},
{caption: 'Zenburn', name: 'zenburn'}
]
export default function hljsTheme () {
return hljsThemeList
}

7
browser/lib/keygen.js Normal file
View File

@@ -0,0 +1,7 @@
const crypto = require('crypto')
const _ = require('lodash')
module.exports = function (length) {
if (!_.isFinite(length)) length = 6
return crypto.randomBytes(length).toString('hex')
}

79
browser/lib/markdown.js Normal file
View File

@@ -0,0 +1,79 @@
import markdownit from 'markdown-it'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
const katex = window.katex
function createGutter (str) {
let lc = (str.match(/\n/g) || []).length
let lines = []
for (let i = 1; i <= lc; i++) {
lines.push('<span>' + i + '</span>')
}
return '<span class="lineNumber">' + lines.join('') + '</span>'
}
var md = markdownit({
typographer: true,
linkify: true,
html: true,
xhtmlOut: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs">' +
createGutter(str) +
'<code>' +
hljs.highlight(lang, str).value +
'</code></pre>'
} catch (e) {}
}
return '<pre class="hljs">' +
createGutter(str) +
'<code>' +
str.replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;') +
'</code></pre>'
}
})
md.use(emoji, {
shortcuts: {}
})
md.use(math, {
inlineRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), {displayMode: true})
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-checkbox'))
let originalRenderToken = md.renderer.renderToken
md.renderer.renderToken = function renderToken (tokens, idx, options) {
let token = tokens[idx]
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
if (token.map != null) {
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
}
return result
}
export default function markdown (content) {
if (content == null) content = ''
return md.render(content.toString())
}

View File

@@ -1,90 +1,9 @@
const modes = [ const modes = [
// Major
{ {
name: 'text', name: 'text',
label: 'Plain text', label: 'Plain text',
mode: 'text' mode: 'text'
}, },
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs'],
mode: 'csharp'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
// Minor
{ {
name: 'abap', name: 'abap',
label: 'ABAP', label: 'ABAP',
@@ -145,6 +64,12 @@ const modes = [
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'], alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
mode: 'batchfile' mode: 'batchfile'
}, },
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{ {
name: 'cirru', name: 'cirru',
label: 'Cirru', label: 'Cirru',
@@ -175,6 +100,24 @@ const modes = [
alias: ['cfm', 'cfc'], alias: ['cfm', 'cfc'],
mode: 'coldfusion' mode: 'coldfusion'
}, },
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{ {
name: 'curly', name: 'curly',
label: 'Curly', label: 'Curly',
@@ -283,6 +226,12 @@ const modes = [
alias: ['opengl', 'shading'], alias: ['opengl', 'shading'],
mode: 'glsl' mode: 'glsl'
}, },
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
{ {
name: 'groovy', name: 'groovy',
label: 'Groovy', label: 'Groovy',
@@ -313,6 +262,12 @@ const modes = [
alias: ['hx', 'hxml'], alias: ['hx', 'hxml'],
mode: 'haxe' mode: 'haxe'
}, },
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{ {
name: 'html_ruby', name: 'html_ruby',
label: 'HTML (Ruby)', label: 'HTML (Ruby)',
@@ -355,6 +310,18 @@ const modes = [
alias: [], alias: [],
mode: 'jade' mode: 'jade'
}, },
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{ {
name: 'json', name: 'json',
label: 'JSON', label: 'JSON',
@@ -451,6 +418,12 @@ const modes = [
alias: [], alias: [],
mode: 'makefile' mode: 'makefile'
}, },
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{ {
name: 'mask', name: 'mask',
label: 'Mask', label: 'Mask',
@@ -529,6 +502,12 @@ const modes = [
alias: ['postgres'], alias: ['postgres'],
mode: 'pgsql' mode: 'pgsql'
}, },
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{ {
name: 'powershell', name: 'powershell',
label: 'PowerShell', label: 'PowerShell',
@@ -559,6 +538,12 @@ const modes = [
alias: ['protocol', 'buffers'], alias: ['protocol', 'buffers'],
mode: 'protobuf' mode: 'protobuf'
}, },
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{ {
name: 'r', name: 'r',
label: 'R', label: 'R',
@@ -571,6 +556,12 @@ const modes = [
alias: [], alias: [],
mode: 'rdoc' mode: 'rdoc'
}, },
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{ {
name: 'rust', name: 'rust',
label: 'Rust', label: 'Rust',
@@ -667,6 +658,12 @@ const modes = [
alias: [], alias: [],
mode: 'svg' mode: 'svg'
}, },
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{ {
name: 'swig', name: 'swig',
label: 'SWIG', label: 'SWIG',

View File

@@ -0,0 +1,17 @@
.root
absolute top bottom right
border-width 1px 0
border-style solid
border-color $ui-borderColor
.empty
height 320px
display flex
align-items center
.empty-message
width 100%
font-size 42px
line-height 72px
text-align center
color $ui-inactive-text-color

View File

@@ -0,0 +1,286 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FolderSelect.styl'
import _ from 'lodash'
class FolderSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
search: '',
optionIndex: -1
}
}
componentDidMount () {
this.value = this.props.value
}
componentDidUpdate () {
this.value = this.props.value
}
handleClick (e) {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
}
handleFocus (e) {
if (this.state.status === 'IDLE') {
this.setState({
status: 'FOCUS'
})
}
}
handleBlur (e) {
if (this.state.status === 'FOCUS') {
this.setState({
status: 'IDLE'
})
}
}
handleKeyDown (e) {
switch (e.keyCode) {
case 13:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: -1
}, () => {
this.refs.search.focus()
})
}
break
case 40:
case 38:
if (this.state.status === 'FOCUS') {
this.setState({
status: 'SEARCH',
optionIndex: 0
}, () => {
this.refs.search.focus()
})
}
break
case 9:
if (e.shiftKey) {
e.preventDefault()
let tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])')
let previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
if (previousEl != null) previousEl.focus()
}
}
}
handleSearchInputBlur (e) {
if (e.relatedTarget !== this.refs.root) {
this.setState({
status: 'IDLE'
})
}
}
handleSearchInputChange (e) {
let { folders } = this.props
let search = this.refs.search.value
let optionIndex = search.length > 0
? _.findIndex(folders, (folder) => {
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i'))
})
: -1
this.setState({
search: this.refs.search.value,
optionIndex: optionIndex
})
}
handleSearchInputKeyDown (e) {
switch (e.keyCode) {
case 40:
e.stopPropagation()
this.nextOption()
break
case 38:
e.stopPropagation()
this.previousOption()
break
case 13:
e.stopPropagation()
this.selectOption()
break
case 27:
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.refs.root.focus()
})
}
}
nextOption () {
let { storages } = this.props
let { optionIndex } = this.state
optionIndex++
if (optionIndex >= folders.length) optionIndex = 0
this.setState({
optionIndex
})
}
previousOption () {
let { folders } = this.props
let { optionIndex } = this.state
optionIndex--
if (optionIndex < 0) optionIndex = folders.length - 1
this.setState({
optionIndex
})
}
selectOption () {
let { folders } = this.props
let optionIndex = this.state.optionIndex
let folder = folders[optionIndex]
if (folder != null) {
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(folder.key)
this.refs.root.focus()
})
}
}
handleOptionClick (storageKey, folderKey) {
return (e) => {
e.stopPropagation()
this.setState({
status: 'FOCUS'
}, () => {
this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus()
})
}
}
setValue (value) {
this.value = value
this.props.onChange()
}
render () {
let { className, storages, value } = this.props
let splitted = value.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let options = []
storages.forEach((storage, index) => {
storage.folders.forEach((folder) => {
options.push({
storage: storage,
folder: folder
})
})
})
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
let optionList = options
.map((option, index) => {
return (
<div styleName={index === this.state.optionIndex
? 'search-optionList-item--active'
: 'search-optionList-item'
}
key={option.storage.key + '-' + option.folder.key}
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)}
>
<span styleName='search-optionList-item-name'
style={{borderColor: option.folder.color}}
>
{option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span>
</span>
</div>
)
})
return (
<div className={_.isString(className)
? 'FolderSelect ' + className
: 'FolderSelect'
}
styleName={this.state.status === 'SEARCH'
? 'root--search'
: this.state.status === 'FOCUS'
? 'root--focus'
: 'root'
}
ref='root'
tabIndex='0'
onClick={(e) => this.handleClick(e)}
onFocus={(e) => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
>
{this.state.status === 'SEARCH'
? <div styleName='search'>
<input styleName='search-input'
ref='search'
value={this.state.search}
placeholder='Folder...'
onChange={(e) => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)}
/>
<div styleName='search-optionList'
ref='optionList'
>
{optionList}
</div>
</div>
: <div styleName='idle'>
<div styleName='idle-label'>
<span styleName='idle-label-name'
style={{borderColor: currentOption.folder.color}}
>
{currentOption.folder.name}
<span styleName='idle-label-name-surfix'>in {currentOption.storage.name}</span>
</span>
</div>
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
</div>
}
</div>
)
}
}
FolderSelect.propTypes = {
className: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({
key: PropTypes.string,
name: PropTypes.string,
color: PropTypes.string
}))
}
export default CSSModules(FolderSelect, styles)

View File

@@ -0,0 +1,88 @@
.root
position relative
border solid 1px transparent
line-height 34px
vertical-align middle
border-radius 2px
transition 0.15s
user-select none
&:hover
background-color white
border-color $ui-borderColor
.root--search, .root--focus
@extend .root
background-color white
border-color $ui-input--focus-borderColor
&:hover
background-color white
border-color $ui-input--focus-borderColor
.idle
position relative
cursor pointer
.idle-label
absolute left top
padding 0 0 0 5px
right 20px
overflow ellipsis
.idle-label-name
border-left solid 4px transparent
padding 2px 5px
.idle-label-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
.idle-caret
absolute right top
height 34px
width 20px
line-height 34px
.search
absolute top left right bottom
line-height 34px
.search-input
vertical-align middle
position relative
top -2px
outline none
border none
height 20px
line-height 20px
background-color transparent
padding 0 10px
.search-optionList
position fixed
border $ui-border
z-index 200
background-color white
border-radius 2px
.search-optionList-item
height 34px
width 250px
box-sizing border-box
padding 0 5px
overflow ellipsis
cursor pointer
&:hover
background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
.search-optionList-item-name
border-left solid 4px transparent
padding 2px 5px
.search-optionList-item-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px

View File

@@ -0,0 +1,291 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownNoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
class MarkdownNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
note: Object.assign({
title: '',
content: '',
isMovingNote: false,
isDeleting: false
}, props.note)
}
this.dispatchTimer = null
}
focus () {
this.refs.content.focus()
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
this.setState({
note: Object.assign({}, nextProps.note),
isDeleting: false
}, () => {
this.refs.content.reload()
this.refs.tags.reset()
})
}
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.content = this.refs.content.value
note.tags = this.refs.tags.value
note.title = this.findTitle(note.content)
note.updatedAt = new Date()
this.setState({
note
}, () => {
this.save()
})
}
save () {
let { note, dispatch } = this.props
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
})
}
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
}
render () {
let { storages, config } = this.props
let { note } = this.state
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
</button>
</div>
</div>
}
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={this.state.note.content}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
</div>
</div>
)
}
}
MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -0,0 +1,90 @@
$info-height = 75px
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-delete
height 80px
clearfix()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
.info-delete-cancelButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
.body
absolute bottom left right
top $info-height
.body-noteEditor
absolute top bottom left right

View File

@@ -0,0 +1,477 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetNoteDetail.styl'
import CodeEditor from 'browser/components/CodeEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import modes from 'browser/lib/modes'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
class SnippetNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0,
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
}),
isDeleting: false
}
}
focus () {
this.refs.description.focus()
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) {
let nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
})
this.setState({
snippetIndex: 0,
note: nextNote,
isDeleting: false
}, () => {
let { snippets } = this.state.note
snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
this.refs.tags.reset()
})
}
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
note.title = this.findTitle(note.description)
this.setState({
note
}, () => {
this.save()
})
}
save () {
let { note, dispatch } = this.props
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
})
}
handleDeleteConfirmButtonClick (e) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
}
handleTabPlusButtonClick (e) {
let { note } = this.state
note.snippets = note.snippets.concat([{
name: '',
mode: 'text',
content: ''
}])
this.setState({
note
})
}
handleTabButtonClick (index) {
return (e) => {
this.setState({
snippetIndex: index
})
}
}
handleTabDeleteButtonClick (index) {
return (e) => {
if (this.state.note.snippets.length > 1) {
let snippets = this.state.note.snippets.slice()
snippets.splice(index, 1)
this.state.note.snippets = snippets
this.setState({
note: this.state.note
})
}
}
}
handleNameInputChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].name = e.target.value
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
handleModeButtonClick (index) {
return (e) => {
let menu = new Menu()
modes.forEach((mode) => {
menu.append(new MenuItem({
label: mode.label,
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
}))
})
menu.popup(remote.getCurrentWindow())
}
}
handleModeOptionClick (index, name) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].mode = name
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
handleCodeChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
render () {
let { storages, config } = this.props
let { note } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(index)(e)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
{note.snippets.length > 1 &&
<button styleName='tabList-item-deleteButton'
onClick={(e) => this.handleTabDeleteButtonClick(index)(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
let mode = snippet.mode === 'text'
? null
: modes.filter((mode) => mode.name === snippet.mode)[0]
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
<div styleName='tabView-top'>
<input styleName='tabView-top-name'
placeholder='Filename including extensions...'
value={snippet.name}
onChange={(e) => this.handleNameInputChange(index)(e)}
/>
<button styleName='tabView-top-mode'
onClick={(e) => this.handleModeButtonClick(index)(e)}
>
{mode == null
? 'Select Syntax...'
: mode.label
}&nbsp;
<i className='fa fa-caret-down'/>
</button>
</div>
<CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
</div>
})
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
</button>
</div>
</div>
}
<div styleName='body'>
<div styleName='body-description'>
<textarea styleName='body-description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={this.state.note.description}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='tabList'>
{tabList}
<button styleName='tabList-plusButton'
onClick={(e) => this.handleTabPlusButtonClick(e)}
>
<i className='fa fa-plus'/>
</button>
</div>
{viewList}
</div>
</div>
)
}
}
SnippetNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -0,0 +1,170 @@
$info-height = 75px
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-delete
height 80px
clearfix()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
.info-delete-cancelButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
.body
absolute bottom left right
top $info-height
.body-description
absolute top left right
height 80px
border-bottom $ui-border
.body-description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
.tabList
absolute left right
top 80px
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList-item
position relative
flex 1
border-right $ui-border
&:hover
.tabList-item-deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
.tabList-item--active
@extend .tabList-item
.tabList-item-button
border-color $brand-color
.tabList-item-button
width 100%
height 100%
navButtonColor()
border-left 4px solid transparent
.tabList-item-deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.tabList-plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-top
absolute top left right
height 30px
border-bottom $ui-border
display flex
.tabView-top-name
flex 1
border none
padding 0 10px
font-size 14px
.tabView-top-mode
width 110px
padding 0
border none
border-left $ui-border
colorDefaultButton()
color $ui-inactive-text-color
&:hover
color $ui-text-color
.tabView-content
absolute left right bottom
top 30px

View File

@@ -0,0 +1,66 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StarButton.styl'
import _ from 'lodash'
class StarButton extends React.Component {
constructor (props) {
super(props)
this.state = {
isActive: false
}
}
handleMouseDown (e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
this.setState({
isActive: false
})
}
render () {
let { className } = this.props
return (
<button className={_.isString(className)
? 'StarButton ' + className
: 'StarButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<i className={this.state.isActive || this.props.isActive
? 'fa fa-star'
: 'fa fa-star-o'
}
/>
</button>
)
}
}
StarButton.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CSSModules(StarButton, styles)

View File

@@ -0,0 +1,11 @@
.root
position relative
padding 0
transition transform 0.15s
&:hover
transform rotate(-72deg)
.root--active
@extend .root
color $ui-active-color
transform rotate(-72deg)

View File

@@ -0,0 +1,148 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
newTag: ''
}
}
componentDidMount () {
this.value = this.props.value
}
componentDidUpdate () {
this.value = this.props.value
}
handleNewTagInputKeyDown (e) {
switch (e.keyCode) {
case 13:
this.submitTag()
break
case 8:
if (this.refs.newTag.value.length === 0) {
this.removeLastTag()
}
}
}
removeLastTag () {
let { value } = this.props
value = _.isArray(value)
? value.slice()
: []
value.pop()
value = _.uniq(value)
this.value = value
this.props.onChange()
}
reset () {
this.setState({
newTag: ''
})
}
submitTag () {
let { value } = this.props
let newTag = this.refs.newTag.value.trim()
if (newTag.length <= 0) {
this.setState({
newTag: ''
})
return
}
value = _.isArray(value)
? value.slice()
: []
value.push(newTag)
value = _.uniq(value)
this.setState({
newTag: ''
}, () => {
this.value = value
this.props.onChange()
})
}
handleNewTagInputChange (e) {
this.setState({
newTag: this.refs.newTag.value
})
}
handleTagRemoveButtonClick (tag) {
return (e) => {
let { value } = this.props
value.splice(value.indexOf(tag), 1)
value = _.uniq(value)
this.value = value
this.props.onChange()
}
}
render () {
let { value, className } = this.props
let tagList = _.isArray(value)
? value.map((tag) => {
return (
<span styleName='tag'
key={tag}
>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
>
<i className='fa fa-times fa-fw'/>
</button>
<span styleName='tag-label'>{tag}</span>
</span>
)
})
: []
return (
<div className={_.isString(className)
? 'TagSelect ' + className
: 'TagSelect'
}
styleName='root'
>
<i styleName='icon'
className='fa fa-tags'
/>
{tagList}
<input styleName='newTag'
ref='newTag'
value={this.state.newTag}
placeholder='Add tag...'
onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
/>
</div>
)
}
}
TagSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
}
export default CSSModules(TagSelect, styles)

View File

@@ -0,0 +1,63 @@
.root
position relative
user-select none
.icon
display inline-block
width 30px
vertical-align middle
text-align center
color $ui-button-color
.tag
display inline-block
margin 0 2px
vertical-align middle
height 20px
background-color white
border-radius 3px
overflow hidden
clearfix()
.tag-removeButton
float left
height 20px
width 18px
margin 0
padding 0
border-style solid
border-color $ui-borderColor
border-width 0 0 0 3px
line-height 18px
background-color transparent
color $ui-button-color
&:hover
background-color $ui-button--hover-backgroundColor
&:active, &:active:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
border-color $ui-button--focus-borderColor
&:focus
border-color $ui-button--focus-borderColor
.tag-label
float left
height 20px
line-height 20px
padding 0 6px
.newTag
display inline-block
margin 0 2px
vertical-align middle
height 24px
box-sizing borde-box
border none
border-bottom $ui-border
background-color transparent
outline none
padding 0 4px
&:focus
border-color $ui-input--focus-borderColor = #369DCD
&:disabled
background-color $ui-input--disabled-backgroundColor = #DDD

View File

@@ -0,0 +1,105 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Detail.styl'
import _ from 'lodash'
import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter'
const OSX = global.process.platform === 'darwin'
class Detail extends React.Component {
constructor (props) {
super(props)
this.focusHandler = () => {
this.refs.root != null && this.refs.root.focus()
}
this.deleteHandler = () => {
this.refs.root != null && this.refs.root.handleDeleteMenuClick()
}
}
componentDidMount () {
ee.on('detail:focus', this.focusHandler)
ee.on('detail:delete', this.deleteHandler)
}
componentWillUnmount () {
ee.off('detail:focus', this.focusHandler)
ee.off('detail:delete', this.deleteHandler)
}
render () {
let { location, notes, config } = this.props
let note = null
if (location.query.key != null) {
let splitted = location.query.key.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let noteKey = splitted.shift()
note = _.find(notes, {
storage: storageKey,
folder: folderKey,
key: noteKey
})
}
if (note == null) {
return (
<div styleName='root'
style={this.props.style}
tabIndex='0'
>
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
if (note.type === 'SNIPPET_NOTE') {
return (
<SnippetNoteDetail
note={note}
config={config}
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'style',
'ignorePreviewPointerEvents',
'location'
])}
/>
)
}
return (
<MarkdownNoteDetail
note={note}
config={config}
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'style',
'ignorePreviewPointerEvents',
'location'
])}
/>
)
}
}
Detail.propTypes = {
dispatch: PropTypes.func,
storages: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(Detail, styles)

View File

@@ -1,291 +0,0 @@
import React, { PropTypes} from 'react'
import { connect } from 'react-redux'
import { CREATE_MODE, EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions'
// import UserNavigator from './HomePage/UserNavigator'
import ArticleNavigator from './HomePage/ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar'
import ArticleList from './HomePage/ArticleList'
import ArticleDetail from './HomePage/ArticleDetail'
import _ from 'lodash'
import keygen from 'boost/keygen'
import { isModalOpen, closeModal } from 'boost/modal'
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
class HomePage extends React.Component {
componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、
// `window`に直接かける
this.keyHandler = e => this.handleKeyDown(e)
window.addEventListener('keydown', this.keyHandler)
}
componentWillUnmount () {
window.removeEventListener('keydown', this.keyHandler)
}
handleKeyDown (e) {
if (isModalOpen()) {
if (e.keyCode === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { nav, top, list, detail } = this.refs
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
e.preventDefault()
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (top.isInputFocused() && !e.metaKey) {
if (e.keyCode === 13 || e.keyCode === 27) top.escape()
return
}
switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE:
if (e.keyCode === 27) {
detail.handleCancelButtonClick()
}
if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) {
detail.handleSaveButtonClick()
}
break
case IDLE_MODE:
if (e.keyCode === 69) {
detail.handleEditButtonClick()
e.preventDefault()
}
if (e.keyCode === 68) {
detail.handleDeleteButtonClick()
}
// `detail`の`openDeleteConfirmMenu`の時。
if (detail.state.openDeleteConfirmMenu) {
if (e.keyCode === 27) {
detail.handleDeleteCancelButtonClick()
}
if (e.keyCode === 13 && e.metaKey) {
detail.handleDeleteConfirmButtonClick()
}
break
}
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
if (e.keyCode === 27 || (e.keyCode === 70 && e.metaKey)) {
top.focusInput()
}
if (e.keyCode === 38) {
list.selectPriorArticle()
}
if (e.keyCode === 40) {
list.selectNextArticle()
}
if (e.keyCode === 65 || e.keyCode === 13 && e.metaKey) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
}
}
render () {
let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
folders={folders}
status={status}
allArticles={allArticles}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
status={status}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
activeArticle={activeArticle}
folders={folders}
status={status}
tags={tags}
filters={filters}
/>
</div>
)
}
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
}
// Build filter object by key
function buildFilter (key) {
if (key.match(/^\/\/.+/)) {
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
}
if (key.match(/^\/.+/)) {
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
}
function remap (state) {
let { folders, articles, status } = state
if (articles == null) articles = []
articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt)
})
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
// Filter articles
let filters = status.search.split(' ')
.map(key => key.trim())
.filter(ignoreInvalidKey)
.map(buildFilter)
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) {
let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`)))
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`)))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) {
articles = articles.filter(article => {
return _.findWhere(targetFolders, {key: article.FolderKey})
})
}
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
}
// Grab active article
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
// remove Unsaved new article if user is not CREATE_MODE
if (status.mode !== CREATE_MODE) {
let targetIndex = _.findIndex(articles, article => article.status === NEW)
if (targetIndex >= 0) articles.splice(targetIndex, 1)
}
// switching CREATE_MODE
// restrict
// 1. team have one folder at least
// or Change IDLE MODE
if (status.mode === CREATE_MODE) {
let newArticle = _.findWhere(articles, {status: 'NEW'})
console.log('targetFolders')
let FolderKey = targetFolders.length > 0
? targetFolders[0].key
: folders[0].key
if (newArticle == null) {
newArticle = {
id: null,
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
status: NEW
}
articles.unshift(newArticle)
}
activeArticle = newArticle
} else if (status.mode === CREATE_MODE) {
status.mode = IDLE_MODE
}
return {
folders,
status,
allArticles,
articles,
activeArticle,
tags,
filters: {
folder: folderFilters,
tag: tagFilters,
text: textFilters
}
}
}
HomePage.propTypes = {
params: PropTypes.shape({
userId: PropTypes.string
}),
status: PropTypes.shape({
userId: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
filters: PropTypes.shape({
folder: PropTypes.array,
tag: PropTypes.array,
text: PropTypes.array
})
}
export default connect(remap)(HomePage)

View File

@@ -1,520 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import ModeIcon from 'boost/components/ModeIcon'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor'
import {
IDLE_MODE,
CREATE_MODE,
EDIT_MODE,
switchMode,
switchArticle,
switchFolder,
clearSearch,
lockStatus,
unlockStatus,
updateArticle,
destroyArticle,
NEW
} from 'boost/actions'
import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark'
import TagLink from 'boost/components/TagLink'
import TagSelect from 'boost/components/TagSelect'
import ModeSelect from 'boost/components/ModeSelect'
import activityRecord from 'boost/activityRecord'
const BRAND_COLOR = '#18AF90'
const editDeleteTutorialElement = (
<svg width='300' height='500' className='tutorial'>
<text x='50' y='220' fill={BRAND_COLOR} fontSize='24'>Edit / Delete a post</text>
<text x='90' y='245' fill={BRAND_COLOR} fontSize='18'>press `e`/`d`</text>
<svg x='150' y='35'>
<path fill='white' d='M87.5,93.6c-16.3-5.7-30.6-16.7-39.9-31.4c-5.5-8.7-9-19.1-3.4-28.7c4.8-8.2,13.6-12.8,22.4-15.3
c15.7-4.5,34.4-6.2,49.7,0.4c17.3,7.4,25.6,26.3,25.7,44.4c0.1,10.4-3.4,20.9-13.1,26c-8.6,4.5-19,4.1-28.4,3.7
c-1.9-0.1-1.9,2.9,0,3c9.3,0.4,19.2,0.6,27.9-3.2c8.5-3.7,13.8-11.2,15.7-20.2c3.6-17.9-2.9-40.2-17.7-51.4
C110.8,9.1,89,9.9,70.8,14c-17.9,4-37.4,16.8-31.3,37.9C45.6,73,66.7,89.5,86.7,96.5C88.6,97.1,89.4,94.2,87.5,93.6L87.5,93.6z'/>
<path fill='white' d='M11.9,89.7c14.8-3.4,29.7-6,44.8-7.9c-0.5-0.6-1-1.3-1.4-1.9c-2.6,6.3-2.8,12.7-0.7,19.2
c0.6,1.8,3.5,1,2.9-0.8c-1.9-6-1.7-11.8,0.7-17.6c0.3-0.8-0.5-2-1.4-1.9c-15.3,1.9-30.6,4.5-45.6,8C9.3,87.3,10.1,90.2,11.9,89.7
L11.9,89.7z'/>
<path fill='white' d='M48.6,81.5c-9.4,10.4-17,22.3-22.2,35.3c-5.5,13.6-9.3,28.9-6,43.4c0.4,1.9,3.3,1.1,2.9-0.8
c-3.2-14,0.7-28.8,6-41.8c5.1-12.5,12.4-24,21.5-34C52,82.2,49.9,80,48.6,81.5L48.6,81.5z'/>
</svg>
</svg>
)
const tagSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
<svg x='0' y='-15'>
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
</svg>
</svg>
)
const modeSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
<svg x='300' y='0'>
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
</svg>
</svg>
)
function makeInstantArticle (article) {
return Object.assign({}, article)
}
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
article: makeInstantArticle(props.activeArticle),
previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false,
isModeChanged: false
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
}
componentDidUpdate (prevProps) {
let isModeChanged = prevProps.status.mode !== this.props.status.mode
if (isModeChanged && this.props.status.mode !== IDLE_MODE) {
ReactDOM.findDOMNode(this.refs.title).focus()
}
}
componentWillReceiveProps (nextProps) {
let nextState = {}
let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key)
let isModeChanged = nextProps.status.mode !== this.props.status.mode
// Reset article input
if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) {
Object.assign(nextState, {
article: makeInstantArticle(nextProps.activeArticle)
})
}
// Clean state
if (isModeChanged) {
Object.assign(nextState, {
openDeleteConfirmMenu: false,
previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false
})
}
this.setState(nextState)
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
Command() + Enter to create a new post
</div>
)
}
handleEditButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(EDIT_MODE))
}
handleDeleteButtonClick (e) {
this.setState({openDeleteConfirmMenu: true})
}
handleDeleteConfirmButtonClick (e) {
let { dispatch, activeArticle } = this.props
dispatch(destroyArticle(activeArticle.key))
activityRecord.emit('ARTICLE_DESTROY')
this.setState({openDeleteConfirmMenu: false})
}
handleDeleteCancelButtonClick (e) {
this.setState({openDeleteConfirmMenu: false})
}
renderIdle () {
let { status, activeArticle, folders } = this.props
let tags = activeArticle.tags != null ? activeArticle.tags.length > 0
? activeArticle.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (
<span className='noTags'>Not tagged yet</span>
) : null
let folder = _.findWhere(folders, {key: activeArticle.FolderKey})
return (
<div className='ArticleDetail idle'>
{this.state.openDeleteConfirmMenu
? (
<div className='deleteConfirm'>
<div className='right'>
Are you sure to delete this article?
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'>
<i className='fa fa-fw fa-check'/> Sure
</button>
<button onClick={e => this.handleDeleteCancelButtonClick(e)}>
<i className='fa fa-fw fa-times'/> Cancel
</button>
</div>
</div>
)
: (
<div className='detailInfo'>
<div className='left'>
<div className='info'>
<FolderMark color={folder.color}/> <span className='folderName'>{folder.name}</span>&nbsp;
Created : {moment(activeArticle.createdAt).format('YYYY/MM/DD')}&nbsp;
Updated : {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
</div>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div>
<div className='right'>
<button onClick={e => this.handleEditButtonClick(e)} className='editBtn'>
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Edit (e)</span>
</button>
<button onClick={e => this.handleDeleteButtonClick(e)} className='deleteBtn'>
<i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete (d)</span>
</button>
</div>
{status.isTutorialOpen ? editDeleteTutorialElement : null}
</div>
)
}
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
<ModeIcon className='mode' mode={activeArticle.mode}/>
<div className='title'>{activeArticle.title}</div>
</div>
{activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/>
: <CodeEditor readOnly onChange={(e, value) => this.handleContentChange(e, value)} mode={activeArticle.mode} code={activeArticle.content}/>
}
</div>
</div>
</div>
)
}
handleCancelButtonClick (e) {
let { activeArticle, dispatch } = this.props
dispatch(unlockStatus())
if (activeArticle.status === NEW) dispatch(switchArticle(null))
dispatch(switchMode(IDLE_MODE))
}
handleSaveButtonClick (e) {
let { dispatch, folders, filters } = this.props
let article = this.state.article
let newArticle = Object.assign({}, article)
let folder = _.findWhere(folders, {key: article.FolderKey})
if (folder == null) return false
dispatch(unlockStatus())
delete newArticle.status
newArticle.updatedAt = new Date()
if (newArticle.createdAt == null) {
newArticle.createdAt = new Date()
activityRecord.emit('ARTICLE_CREATE')
} else {
activityRecord.emit('ARTICLE_UPDATE')
}
dispatch(updateArticle(newArticle))
dispatch(switchMode(IDLE_MODE))
// Folder filterがかかっている時に、
// Searchを初期化し、更新先のFolder filterをかける
// かかれていない時に
// Searchを初期化する
if (filters.folder.length !== 0) dispatch(switchFolder(folder.name))
else dispatch(clearSearch())
dispatch(switchArticle(newArticle.key))
}
handleFolderKeyChange (e) {
let article = this.state.article
article.FolderKey = e.target.value
this.setState({article: article})
}
handleTitleChange (e) {
let { article } = this.state
article.title = e.target.value
let _isTitleChanged = article.title !== this.props.activeArticle.title
let { isTagChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTitleChanged || isTagChanged || isContentChanged || isModeChanged
this.setState({
article,
isTitleChanged: _isTitleChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleTagsChange (newTag, tags) {
let article = this.state.article
article.tags = tags
this.setState({article: article})
let _isTagChanged = _.difference(article.tags, this.props.activeArticle.tags).length > 0 || _.difference(this.props.activeArticle.tags, article.tags).length > 0
let { isTitleChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTagChanged || isTitleChanged || isContentChanged || isModeChanged
this.setState({
article,
isTagChanged: _isTagChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleModeChange (value) {
let { article } = this.state
article.mode = value
let _isModeChanged = article.mode !== this.props.activeArticle.mode
let { isTagChanged, isContentChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isModeChanged || isTagChanged || isContentChanged || isTitleChanged
this.setState({
article,
previewMode: false,
isModeChanged: _isModeChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleModeSelectBlur () {
if (this.refs.code != null) {
this.refs.code.editor.focus()
}
}
handleContentChange (e, value) {
let { status } = this.props
if (status.mode === IDLE_MODE) {
return
}
let { article } = this.state
article.content = value
let _isContentChanged = article.content !== this.props.activeArticle.content
let { isTagChanged, isModeChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isContentChanged || isTagChanged || isModeChanged || isTitleChanged
this.setState({
article,
isContentChanged: _isContentChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
}
handleTogglePreviewButtonClick (e) {
this.setState({previewMode: !this.state.previewMode})
}
handleTitleKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.refs.mode.handleIdleSelectClick()
}
}
renderEdit () {
let { folders, status, tags } = this.props
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
return (
<div className='ArticleDetail edit'>
<div className='detailInfo'>
<div className='left'>
<select
className='folder'
value={this.state.article.FolderKey}
onChange={e => this.handleFolderKeyChange(e)}
>
{folderOptions}
</select>
{this.state.isArticleEdited ? ' (edited)' : ''}
<TagSelect
tags={this.state.article.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/>
{status.isTutorialOpen ? tagSelectTutorialElement : null}
</div>
<div className='right'>
{
this.state.article.mode === 'markdown'
? (<button className='preview' onClick={e => this.handleTogglePreviewButtonClick(e)}>{!this.state.previewMode ? 'Preview' : 'Edit'}</button>)
: null
}
<button onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
<button onClick={e => this.handleSaveButtonClick(e)} className='primary'>Save</button>
</div>
</div>
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
<div className='title'>
<input onKeyDown={e => this.handleTitleKeyDown(e)} placeholder='Title' ref='title' value={this.state.article.title} onChange={e => this.handleTitleChange(e)}/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
value={this.state.article.mode}
className='mode'
onBlur={() => this.handleModeSelectBlur()}
/>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
</div>
{this.state.previewMode
? <MarkdownPreview content={this.state.article.content}/>
: (<CodeEditor
ref='code'
onChange={(e, value) => this.handleContentChange(e, value)}
readOnly={false}
mode={this.state.article.mode}
code={this.state.article.content}
/>)
}
</div>
</div>
</div>
)
}
render () {
let { status, activeArticle } = this.props
if (activeArticle == null) return this.renderEmpty()
switch (status.mode) {
case CREATE_MODE:
case EDIT_MODE:
return this.renderEdit()
case IDLE_MODE:
default:
return this.renderIdle()
}
}
}
ArticleDetail.propTypes = {
status: PropTypes.shape(),
activeArticle: PropTypes.shape(),
activeUser: PropTypes.shape(),
dispatch: PropTypes.func
}
ArticleDetail.prototype.linkState = linkState

View File

@@ -1,118 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import moment from 'moment'
import { switchArticle, NEW } from 'boost/actions'
import FolderMark from 'boost/components/FolderMark'
import TagLink from 'boost/components/TagLink'
import _ from 'lodash'
export default class ArticleList extends React.Component {
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
}
componentDidUpdate () {
let { articles, activeArticle } = this.props
var index = articles.indexOf(activeArticle)
var el = ReactDOM.findDOMNode(this)
var li = el.querySelectorAll('.ArticleList>div')[index]
if (li == null) {
return
}
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
if (overflowBelow) {
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
}
var overflowAbove = el.scrollTop > li.offsetTop
if (overflowAbove) {
el.scrollTop = li.offsetTop
}
}
// 移動ができなかったらfalseを返す:
selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) - 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
selectNextArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) + 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key))
}
}
render () {
let { articles, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'>
{folder != null
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span>
: <span><FolderMark color={-1}/>Unknown</span>
}
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span>
</div>
<div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== NEW ? article.title : '(New article)'}</div>
</div>
<div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div>
</div>
<div className='divider'></div>
</div>
)
})
return (
<div className='ArticleList'>
{articleElements}
</div>
)
}
}
ArticleList.propTypes = {
folders: PropTypes.array,
articles: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func
}

View File

@@ -1,160 +0,0 @@
import React, { PropTypes } from 'react'
import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions'
import { openModal } from 'boost/modal'
import FolderMark from 'boost/components/FolderMark'
import Preferences from 'boost/components/modal/Preferences'
import CreateNewFolder from 'boost/components/modal/CreateNewFolder'
import remote from 'remote'
let userName = remote.getGlobal('process').env.USER
const BRAND_COLOR = '#18AF90'
const preferenceTutorialElement = (
<svg width='300' height='300' className='tutorial'>
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
<svg x='-30' y='-270' width='400' height='400'>
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
</svg>
</svg>
)
const newPostTutorialElement = (
<svg width='900' height='900' className='tutorial'>
<text x='290' y='155' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
<text x='300' y='180' fill={BRAND_COLOR} fontSize='16'>press `⌘ + Enter` or `a`</text>
<svg x='130' y='-20' width='400' height='400'>
<path fill='white' d='M56.2,132.5c11.7-2.9,23.9-6,36.1-4.1c8.7,1.4,16.6,5.5,23.7,10.5c13.3,9.4,24.5,21.5,40.2,27
c1.8,0.6,2.6-2.3,0.8-2.9c-17.1-6-28.9-20.3-44-29.7c-7-4.4-14.8-7.4-23-8.2c-11.7-1.1-23.3,1.7-34.5,4.5
C53.6,130.1,54.4,133,56.2,132.5L56.2,132.5 z'/>
</svg>
<svg x='130' y='-120' width='400' height='400'>
<path fill='white' d='M82.6,218c-7.7,4.5-15.3,9.3-22.7,14.3c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3
C85.7,219.7,84.2,217.1,82.6,218L82.6,218z'/>
</svg>
</svg>
)
const newFolderTutorialElement = (
<svg width='800' height='500' className='tutorial'>
<text x='145' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<svg x='115' y='-10' width='300' height='400'>
<path fill='white' d='M36.6,3.7C28.8,8.2,21.3,13,13.9,18c-1,0.7-0.9,2.4,0.4,2.7c6.2,1.8,11.5,4.8,16.2,9.2
c1.4,1.3,3.5-0.8,2.1-2.1c-5.1-4.8-10.9-8.1-17.6-10c0.1,0.9,0.2,1.8,0.4,2.7c7.4-5,15-9.8,22.7-14.3C39.7,5.3,38.2,2.7,36.6,3.7
L36.6,3.7z'/>
<path fill='white' d='M16.8,21.5c13.3-6.9,29.5-7,42.6,0.6c5.6,3.2,10.4,7.7,14.1,13c3.8,5.4,10.3,16.2,2.2,20.6
c-1.2,0.7-2.5,1.2-3.9,1.6c-1.1,0.4-2.3,0.5-3.4,0.5c-1.3-1.4-2.6-2.8-3.9-4.2c-0.2-4.6,7.5-6,10.5-5.8
c7.4,0.7,13.7,6.2,18.4,11.6c9.4,10.7,14.7,24.3,15.6,38.5c0.1,1.9,3.1,1.9,3,0c-0.9-15.5-6.9-30.4-17.5-41.8
c-6.8-7.3-25.8-19.1-32.3-4.8c-1.9,4.1,0.3,8.5,4.8,9.4c4.6,0.8,11.6-1.8,14.3-5.7c3.6-5.3-0.1-12.8-2.8-17.6
c-3.4-6.1-8.2-11.3-13.8-15.4C50.2,11.6,31,10.9,15.3,19C13.6,19.8,15.1,22.4,16.8,21.5L16.8,21.5z'/>
</svg>
</svg>
)
export default class ArticleNavigator extends React.Component {
handlePreferencesButtonClick (e) {
openModal(Preferences)
}
handleNewPostButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(CREATE_MODE))
}
handleNewFolderButton (e) {
let { activeUser } = this.props
openModal(CreateNewFolder, {user: activeUser})
}
handleFolderButtonClick (name) {
return e => {
let { dispatch } = this.props
dispatch(switchFolder(name))
}
}
handleAllFoldersButtonClick (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(''))
}
render () {
let { status, folders, allArticles } = this.props
let { targetFolders } = status
if (targetFolders == null) targetFolders = []
let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key).length
return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
</button>
)
})
return (
<div className='ArticleNavigator'>
<div className='userInfo'>
<div className='userProfileName'>{userName}</div>
<div className='userName'>localStorage</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
<i className='fa fa-fw fa-chevron-down'/>
<span className='tooltip'>Preferences</span>
</button>
{status.isTutorialOpen ? preferenceTutorialElement : null}
</div>
<div className='controlSection'>
<button onClick={e => this.handleNewPostButtonClick(e)} className='newPostBtn'>
New Post
<span className='tooltip'>Create a new Post ( + Enter or a)</span>
</button>
{status.isTutorialOpen ? newPostTutorialElement : null}
</div>
<div className='folders'>
<div className='header'>
<div className='title'>Folders</div>
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
<i className='fa fa-fw fa-plus'/>
<span className='tooltip'>Create a new folder</span>
</button>
{status.isTutorialOpen ? newFolderTutorialElement : null}
</div>
<div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
{folderElememts}
</div>
</div>
</div>
)
}
}
ArticleNavigator.propTypes = {
activeUser: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
status: PropTypes.shape({
folderId: PropTypes.number
}),
dispatch: PropTypes.func
}

View File

@@ -1,164 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ExternalLink from 'boost/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial } from 'boost/actions'
const BRAND_COLOR = '#18AF90'
const searchTutorialElement = (
<svg width='750' height='120' className='tutorial'>
<text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
<text x='450' y='60' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='450' y='85' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : /{folder_name}\n'}</text>
<text x='465' y='105' fill={BRAND_COLOR} fontSize='14'>
{'exact match : //{folder_name}'}</text>
<svg width='500' height='300'>
<path fill='white' d='M54.5,51.5c-12.4,3.3-27.3-1.4-38.4-7C11.2,42,5,38.1,5.6,31.8c0.7-6.9,8.1-11.2,13.8-13.7
c12.3-5.4,26.4-6.8,39.7-7.7C72.4,9.6,85.7,9.7,99,9.8c55.2,0.3,110.4,2.2,165.5-1.5C291,6.5,317.7,3.8,344.1,7
c12.8,1.6,25.8,4.4,37.5,10c1.2,0.6,2.4,1.1,3.5,1.8c2.4,1.4,3.2,1.5,3.3,4.5c0.1,3.6-2.3,5.9-4.8,8.3c-3.9,3.8-8.6,6.8-13.5,9.2
c-12.6,6-26.5,7.2-40.3,7.7c-13.7,0.5-27.5,0.6-41.2,1.1c-27.7,0.9-55.3,2.2-82.9,4c-30.8,2-61.6,4.5-92.3,7.6
c-15.4,1.5-30.8,3.7-46.3,4.9c-13.6,1.1-30.7,1.5-41.8-7.8c-1.5-1.2-3.6,0.9-2.1,2.1c8.9,7.5,21.4,9.2,32.7,9.2
c15.3,0,30.6-2.6,45.8-4.2c31.3-3.3,62.7-6,94.2-8.1c30.9-2.1,61.8-3.7,92.8-4.7c15.7-0.5,31.4-0.5,47-1.3
c13.1-0.7,26.3-2.7,38.1-8.9c4.4-2.3,8.5-5.1,12-8.6c2.8-2.8,7.3-7.3,6.4-11.7c-0.8-4.3-6.4-6.3-9.8-7.9
c-5.6-2.6-11.4-4.6-17.3-6.2c-28.3-7.5-58.1-5.6-87-3.6c-62.3,4.4-124.5,2.6-187,2.4c-16.4,0-32.8,0-49,2.4
C29.9,11,13.4,13.8,5.5,24.6c-7.3,10,0.7,18.4,9.8,22.9c11.9,5.8,26.9,10.4,40,7C57.2,53.9,56.4,51,54.5,51.5L54.5,51.5z'/>
<path fill='white' d='M446.5,21.4c-9.1-1.6-18.1-3.5-27.4-3.5c-10.2,0-20.4,1.4-30.5,2.8c-1.9,0.3-1.9,3.3,0,3
c9.5-1.3,19.1-2.6,28.8-2.7c9.6-0.2,18.9,1.7,28.3,3.4C447.6,24.7,448.4,21.8,446.5,21.4L446.5,21.4z'/>
</svg>
</svg>
)
export default class ArticleTopBar extends React.Component {
constructor (props) {
super(props)
this.state = {
isTooltipHidden: true
}
}
componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
}
componentWillUnmount () {
this.searchInput.removeEventListener('keydown', this.showTooltip)
this.searchInput.removeEventListener('focus', this.showTooltip)
this.searchInput.removeEventListener('blur', this.showTooltip)
}
handleTooltipRequest (e) {
if (this.searchInput.value.length === 0 && (document.activeElement === this.searchInput)) {
this.setState({isTooltipHidden: false})
} else {
this.setState({isTooltipHidden: true})
}
}
isInputFocused () {
return document.activeElement === ReactDOM.findDOMNode(this.refs.searchInput)
}
escape () {
let { status, dispatch } = this.props
if (status.search.length > 0) {
dispatch(clearSearch())
return
}
this.blurInput()
}
focusInput () {
this.searchInput.focus()
}
blurInput () {
this.searchInput.blur()
}
handleSearchChange (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(e.target.value))
this.handleTooltipRequest()
}
handleSearchClearButton (e) {
this.searchInput.value = ''
this.focusInput()
}
handleTutorialButtonClick (e) {
let { dispatch } = this.props
dispatch(toggleTutorial())
}
render () {
let { status } = this.props
return (
<div className='ArticleTopBar'>
<div className='left'>
<div className='search'>
<i className='fa fa-search fa-fw' />
<input
ref='searchInput'
onFocus={e => this.handleSearchChange(e)}
onBlur={e => this.handleSearchChange(e)}
value={this.props.status.search}
onChange={e => this.handleSearchChange(e)}
placeholder='Search'
type='text'
/>
{
this.props.status.search != null && this.props.status.search.length > 0
? <button onClick={e => this.handleSearchClearButton(e)} className='searchClearBtn'><i className='fa fa-times'/></button>
: null
}
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
- Search by tag : #{'{string}'}<br/>
- Search by folder : /{'{folder_name}'}
</div>
</div>
{status.isTutorialOpen ? searchTutorialElement : null}
</div>
<div className='right'>
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
</button>
<ExternalLink className='logo' href='http://b00st.io'>
<img src='../../resources/favicon-230x230.png' width='44' height='44'/>
<span className='tooltip'>Boost official page</span>
</ExternalLink>
</div>
{status.isTutorialOpen ? (
<div className='tutorial'>
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
<svg width='500' height='250' className='finder'>
<text x='100' y='25' fontSize='32' fill={BRAND_COLOR}>Also, you can open Finder!!</text>
<text x='120' y='55' fontSize='18' fill={BRAND_COLOR}>with pressing `Control` + `shift` + `tab`</text>
</svg>
<svg width='450' className='global'>
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
<text x='50' y='75' fontSize='18' fill={BRAND_COLOR}>Press any key or click to escape tutorial mode</text>
</svg>
<div className='back'></div>
</div>
) : null}
</div>
)
}
}
ArticleTopBar.propTypes = {
search: PropTypes.string,
dispatch: PropTypes.func,
status: PropTypes.shape({
search: PropTypes.string
})
}

View File

@@ -1,52 +0,0 @@
import React, { Component, PropTypes } from 'react'
import { Link } from 'react-router'
import ProfileImage from 'boost/components/ProfileImage'
import { openModal } from 'boost/modal'
import CreateNewTeam from 'boost/components/modal/CreateNewTeam'
export default class UserNavigator extends Component {
handleClick (e) {
openModal(CreateNewTeam)
}
// for dev
componentDidMount () {
// openModal(CreateNewTeam)
}
renderUserList () {
if (this.props.users == null) return null
var users = this.props.users.map((user, index) => (
<li key={'user-' + user.id}>
<Link to={'/users/' + user.id} activeClassName='active'>
<ProfileImage email={user.email} size='44'/>
<div className='userTooltip'>{user.name}</div>
{index < 9 ? <div className='keyLabel'>{'⌘' + (index + 1)}</div> : null}
</Link>
</li>
))
return (
<ul className='userList'>
{users}
</ul>
)
}
render () {
return (
<div className='UserNavigator'>
{this.renderUserList()}
<button className='createTeamBtn' onClick={e => this.handleClick(e)}>
+
<div className='tooltip'>Create a new team</div>
</button>
</div>
)
}
}
UserNavigator.propTypes = {
users: PropTypes.array
}

View File

@@ -1,93 +0,0 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import linkState from 'boost/linkState'
import { login } from 'boost/api'
import auth from 'boost/auth'
export default class LoginPage extends React.Component {
constructor (props) {
super(props)
this.state = {
user: {},
isSending: false,
error: null
}
this.linkState = linkState
}
handleSubmit (e) {
e.preventDefault()
this.setState({
isSending: true,
error: null
}, function () {
login(this.state.user)
.then(res => {
let { user, token } = res.body
auth.user(user, token)
this.props.history.pushState('home')
})
.catch(err => {
console.error(err)
if (err.code === 'ECONNREFUSED') {
return this.setState({
error: {
name: 'CunnectionRefused',
message: 'Can\'t cznnect to API server.'
},
isSending: false
})
} else if (err.status != null) {
return this.setState({
error: {
name: err.response.body.name,
message: err.response.body.message
},
isSending: false
})
}
else throw err
})
})
}
render () {
return (
<div className='LoginContainer'>
<img className='logo' src='../../resources/favicon-230x230.png'/>
<nav className='authNavigator text-center'>
<Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link>
</nav>
<form onSubmit={e => this.handleSubmit(e)}>
<div className='formField'>
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.password')} onChange={this.handleChange} type='password' placeholder='Password'/>
</div>
{this.state.isSending
? (
<p className='alertInfo'>Logging in...</p>
) : null}
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
<div className='formField'>
<button className='logInButton' type='submit'>Log In</button>
</div>
</form>
</div>
)
}
}
LoginPage.propTypes = {
history: PropTypes.shape({
pushState: PropTypes.func
})
}

159
browser/main/Main.js Normal file
View File

@@ -0,0 +1,159 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Main.styl'
import { connect } from 'react-redux'
import SideNav from './SideNav'
import TopBar from './TopBar'
import NoteList from './NoteList'
import Detail from './Detail'
import dataApi from 'browser/main/lib/dataApi'
import StatusBar from './StatusBar'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
class Main extends React.Component {
constructor (props) {
super(props)
let { config } = props
this.state = {
isSliderFocused: false,
listWidth: config.listWidth
}
}
componentDidMount () {
let { dispatch } = this.props
// Reload all data
dataApi.init()
.then((data) => {
dispatch({
type: 'INIT_ALL',
storages: data.storages,
notes: data.notes
})
if (data.storages.length < 1) {
modal.open(InitModal)
}
})
}
handleSlideMouseDown (e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
handleMouseUp (e) {
if (this.state.isSliderFocused) {
this.setState({
isSliderFocused: false
}, () => {
let { dispatch } = this.props
let newListWidth = this.state.listWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({listWidth: newListWidth})
dispatch({
type: 'SET_LIST_WIDTH',
listWidth: newListWidth
})
})
}
}
handleMouseMove (e) {
if (this.state.isSliderFocused) {
let offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
} else if (newListWidth > 600) {
newListWidth = 600
}
this.setState({
listWidth: newListWidth
})
}
}
render () {
let { config } = this.props
return (
<div
className='Main'
styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, [
'dispatch',
'storages',
'config',
'location'
])}
/>
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
ref='body'
>
<TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'config',
'notes',
'params',
'location'
])}
/>
<NoteList style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'config',
'params',
'location'
])}
/>
<div styleName={this.state.isSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.listWidth}}
onMouseDown={(e) => this.handleSlideMouseDown(e)}
draggable='false'
>
<div styleName='slider-hitbox'/>
</div>
<Detail
style={{left: this.state.listWidth + 1}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'config',
'params',
'location'
])}
ignorePreviewPointerEvents={this.state.isSliderFocused}
/>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
/>
</div>
)
}
}
Main.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array
}
export default connect((x) => x)(CSSModules(Main, styles))

30
browser/main/Main.styl Normal file
View File

@@ -0,0 +1,30 @@
.root
absolute top left bottom right
.body
absolute right top
bottom $statusBar-height - 1
left $sideNav-width
.body--expanded
@extend .body
left $sideNav--folded-width
.slider
absolute top bottom
width 1px
background-color $ui-borderColor
border-width 0
border-style solid
border-color $ui-borderColor
.slider--active
@extend .slider
background-color $ui-button--active-backgroundColor
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize

View File

@@ -1,44 +0,0 @@
import ipc from 'ipc'
import React, { PropTypes } from 'react'
var ContactModal = require('boost/components/modal/ContactModal')
export default class MainContainer extends React.Component {
constructor (props) {
super(props)
this.state = {updateAvailable: false}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
ipc.send('update-app', 'Deal with it.')
}
openContactModal () {
this.openModal(ContactModal)
}
render () {
return (
<div className='Main'>
{this.state.updateAvailable ? (
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
) : null}
{/* <button onClick={this.openContactModal} className='contactButton'>
<i className='fa fa-paper-plane-o'/>
<div className='tooltip'>Contact us</div>
</button> */}
{this.props.children}
</div>
)
}
}
MainContainer.propTypes = {
children: PropTypes.element
}

View File

@@ -0,0 +1,168 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import api from 'browser/lib/api'
import clientKey from 'browser/lib/clientKey'
import activityRecord from 'browser/lib/activityRecord'
const clipboard = require('electron').clipboard
function notify (...args) {
return new window.Notification(...args)
}
function getDefault () {
return {
openDropdown: false,
isSharing: false,
// Fetched url
url: null,
// for tooltip Copy -> Copied!
copied: false,
failed: false
}
}
export default class ShareButton extends React.Component {
constructor (props) {
super(props)
this.state = getDefault()
}
componentWillReceiveProps (nextProps) {
this.setState(getDefault())
}
componentDidMount () {
this.dropdownInterceptor = e => {
this.dropdownClicked = true
}
ReactDOM.findDOMNode(this.refs.dropdown).addEventListener('click', this.dropdownInterceptor)
this.shareViaPublicURLHandler = e => {
this.handleShareViaPublicURLClick(e)
}
}
componentWillUnmount () {
document.removeEventListener('click', this.dropdownHandler)
ReactDOM.findDOMNode(this.refs.dropdown).removeEventListener('click', this.dropdownInterceptor)
}
handleOpenButtonClick (e) {
this.openDropdown()
if (this.dropdownHandler == null) {
this.dropdownHandler = e => {
if (!this.dropdownClicked) {
this.closeDropdown()
} else {
this.dropdownClicked = false
}
}
}
document.removeEventListener('click', this.dropdownHandler)
document.addEventListener('click', this.dropdownHandler)
}
openDropdown () {
this.setState({openDropdown: true})
}
closeDropdown () {
document.removeEventListener('click', this.dropdownHandler)
this.setState({openDropdown: false})
}
handleClipboardButtonClick (e) {
activityRecord.emit('MAIN_DETAIL_COPY')
clipboard.writeText(this.props.article.content)
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!'
})
this.setState({openDropdown: false})
}
handleShareViaPublicURLClick (e) {
let { user } = this.props
let input = Object.assign({}, this.props.article, {
clientKey: clientKey.get(),
writerName: user.name
})
this.setState({
isSharing: true,
failed: false
}, () => {
api.shareViaPublicURL(input)
.then(res => {
let url = res.body.url
this.setState({url: url, isSharing: false})
activityRecord.emit('ARTICLE_SHARE')
})
.catch(err => {
console.log(err)
this.setState({isSharing: false, failed: true})
})
})
}
handleCopyURLClick () {
clipboard.writeText(this.state.url)
this.setState({copied: true})
}
// Restore copy url tooltip
handleCopyURLMouseLeave () {
this.setState({copied: false})
}
render () {
let hasPublicURL = this.state.url != null
return (
<div className='ShareButton'>
<button ref='openButton' onClick={e => this.handleOpenButtonClick(e)} className='ShareButton-open-button'>
<i className='fa fa-fw fa-share-alt'/>
{
this.state.openDropdown ? null : (
<span className='tooltip'>Share</span>
)
}
</button>
<div ref='dropdown' className={'ShareButton-dropdown' + (this.state.openDropdown ? '' : ' hide')}>
{
!hasPublicURL ? (
<button
onClick={e => this.shareViaPublicURLHandler(e)}
ref='sharePublicURL'
disabled={this.state.isSharing}>
<i className='fa fa-fw fa-external-link'/> {this.state.failed ? 'Failed : Click to Try again' : !this.state.isSharing ? 'Share via public URL' : 'Sharing...'}
</button>
) : (
<div className='ShareButton-url'>
<input className='ShareButton-url-input' value={this.state.url} readOnly/>
<button
onClick={e => this.handleCopyURLClick(e)}
className='ShareButton-url-button'
onMouseLeave={e => this.handleCopyURLMouseLeave(e)}
>
<i className='fa fa-fw fa-clipboard'/>
<div className='ShareButton-url-button-tooltip'>{this.state.copied ? 'Copied!' : 'Copy URL'}</div>
</button>
<div className='ShareButton-url-alert'>This url is valid for 7 days.</div>
</div>
)
}
<button onClick={e => this.handleClipboardButtonClick(e)}>
<i className='fa fa-fw fa-clipboard'/>&nbsp;Copy to clipboard
</button>
</div>
</div>
)
}
}
ShareButton.propTypes = {
article: PropTypes.shape({
publicURL: PropTypes.string,
content: PropTypes.string
}),
user: PropTypes.shape({
name: PropTypes.string
})
}

View File

@@ -0,0 +1,93 @@
.root
absolute left bottom
border-top $ui-border
border-bottom $ui-border
overflow auto
top $topBar-height - 1
.item
position relative
height 80px
border-bottom $ui-border
padding 0 5px
user-select none
cursor pointer
transition background-color 0.15s
&:hover
background-color alpha($ui-active-color, 10%)
.item--active
@extend .item
.item-border
border-color $ui-active-color
.item-border
absolute top bottom left right
border-style solid
border-width 2px
border-color transparent
transition 0.15s
.item-info
height 30px
clearfix()
font-size 12px
color $ui-inactive-text-color
line-height 30px
overflow-y hidden
.item-info-left
float left
overflow ellipsis
.item-info-left-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
.item-info-left-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.item-info-right
float right
.item-title
height 20px
line-height 20px
padding 0 5px 0 0
font-weight bold
overflow ellipsis
color $ui-text-color
.item-title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.item-title-empty
font-weight normal
color $ui-inactive-text-color
.item-tagList
height 30px
font-size 12px
line-height 30px
overflow ellipsis
.item-tagList-icon
vertical-align middle
color $ui-button-color
.item-tagList-item
margin 0 4px
padding 0 4px
height 20px
border-radius 3px
vertical-align middle
border-style solid
border-color $ui-borderColor
border-width 0 0 0 3px
background-color $ui-backgroundColor
.item-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -0,0 +1,299 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.selectNextNoteHandler = () => {
console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
this.selectPriorNote()
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
}
componentDidUpdate () {
let { location } = this.props
if (this.notes.length > 0 && location.query.key == null) {
let { router } = this.context
router.replace({
pathname: location.pathname,
query: {
key: this.notes[0].uniqueKey
}
})
return
}
// Auto scroll
if (_.isString(location.query.key)) {
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex > -1) {
let list = this.refs.root
let item = list.childNodes[targetIndex]
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
}
let overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
}
selectPriorNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex === 0) {
return
}
targetIndex--
if (targetIndex < 0) targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
}
})
}
selectNextNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
})
if (targetIndex === this.notes.length - 1) {
return
}
targetIndex++
if (targetIndex < 0) targetIndex = 0
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
}
})
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
// if (e.keyCode === 65 && !e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('top-new-post')
// }
// if (e.keyCode === 65 && e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('nav-new-folder')
// }
// if (e.keyCode === 68) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-delete')
// }
// if (e.keyCode === 84) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-title')
// }
// if (e.keyCode === 69) {
// e.preventDefault()
// }
// if (e.keyCode === 83) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-save')
// }
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorNote()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextNote()
}
}
getNotes () {
let { storages, notes, params, location } = this.props
if (location.pathname.match(/\/home/)) {
return notes
}
if (location.pathname.match(/\/starred/)) {
return notes
.filter((note) => note.isStarred)
}
let storageKey = params.storageKey
let folderKey = params.folderKey
let storage = _.find(storages, {key: storageKey})
if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) {
return notes
.filter((note) => note.storage === storageKey)
}
return notes
.filter((note) => note.folder === folderKey)
}
handleNoteClick (uniqueKey) {
return (e) => {
let { router } = this.context
let { location } = this.props
router.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
}
}
render () {
let { location, storages, notes } = this.props
this.notes = notes = this.getNotes()
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
let noteList = notes
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
let tagElements = _.isArray(note.tags)
? note.tags.map((tag) => {
return (
<span styleName='item-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
let isActive = location.query.key === note.uniqueKey
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={note.uniqueKey}
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)}
>
<div styleName='item-border'/>
<div styleName='item-info'>
<div styleName='item-info-left'>
<span styleName='item-info-left-folder'
style={{borderColor: folder.color}}
>
{folder.name}
<span styleName='item-info-left-folder-surfix'>in {storage.name}</span>
</span>
</div>
<div styleName='item-info-right'>
{moment(note.updatedAt).fromNow()}
</div>
</div>
<div styleName='item-title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code'/>
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o'/>
}
{note.title.trim().length > 0
? note.title
: <span styleName='item-title-empty'>Empty</span>
}
</div>
<div styleName='item-tagList'>
<i styleName='item-tagList-icon'
className='fa fa-tags fa-fw'
/>
{tagElements.length > 0
? tagElements
: <span styleName='item-tagList-empty'>Not tagged yet</span>
}
</div>
</div>
)
})
return (
<div className='NoteList'
styleName='root'
ref='root'
tabIndex='0'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
style={this.props.style}
>
{noteList}
</div>
)
}
}
NoteList.contextTypes = {
router: PropTypes.shape([])
}
NoteList.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
width: PropTypes.number
})
}
export default CSSModules(NoteList, styles)

View File

@@ -0,0 +1,122 @@
.root
absolute top left
bottom $statusBar-height - 1
width $sideNav-width
border-right $ui-border
border-bottom $ui-border
background-color $ui-backgroundColor
user-select none
color $ui-text-color
.top
height $topBar-height
border-bottom $ui-border
.top-menu
navButtonColor()
height $topBar-height - 1
padding 0 10px
font-size 14px
width 100%
text-align left
.top-menu-label
margin-left 5px
.menu
margin-top 15px
.menu-button
navButtonColor()
height 44px
padding 0 10px
font-size 14px
width 100%
text-align left
.menu-button--active
@extend .menu-button
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.menu-button-label
margin-left 5px
.storageList
absolute left right
bottom 44px
top 178px
overflow-y auto
.storageList-empty
padding 0 10px
margin-top 15px
line-height 24px
color $ui-inactive-text-color
.navToggle
navButtonColor()
display block
position absolute
right 5px
bottom 5px
border-radius 16.5px
height 34px
width 34px
line-height 32px
padding 0
.root--folded
@extend .root
width 44px
.storageList-empty
white-space nowrap
transform rotate(90deg)
.top-menu
width 44px - 1
text-align center
&:hover .top-menu-label
width 100px
.top-menu-label
position fixed
display inline-block
height 34px
left 44px
width 0
margin-top -5px
margin-left 0
overflow hidden
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
pointer-events none
.menu-button, .menu-button--active
width 44px - 1
text-align center
&:hover .menu-button-label
width 100px
// TODO: extract tooltip style to a mixin
.menu-button-label
position fixed
display inline-block
height 34px
left 44px
width 0
padding-left 0
margin-top -9px
margin-left 0
overflow ellipsis
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
pointer-events none

View File

@@ -0,0 +1,95 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
class StorageItem extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleHeaderInfoClick (e) {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key)
}
handleFolderButtonClick (folderKey) {
return (e) => {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
}
}
render () {
let { storage, location } = this.props
let folderList = storage.folders.map((folder) => {
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))
return <button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
key={folder.key}
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
>
<span styleName='folderList-item-name'
style={{borderColor: folder.color}}
>
{folder.name}
</span>
</button>
})
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
return (
<div styleName='root'
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}>
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
}
/>
</button>
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
{storage.name}
</span>
<span styleName='header-info-path'>
({storage.path})
</span>
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
{folderList}
</div>
}
</div>
)
}
}
StorageItem.propTypes = {
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,89 @@
.root
width 100%
user-select none
.header
position relative
height 30px
width 100%
&:hover
background-color $ui-button--hover-backgroundColor
&:active
.header-toggleButton
color white
.header--active
@extend .header
.header-info
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-toggleButton
color white
&:active
color white
.header-toggleButton
position absolute
left 0
width 25px
height 30px
padding 0
border none
color $ui-inactive-text-color
background-color transparent
&:hover
color $ui-text-color
&:active
color $ui-active-color
.header-info
display block
width 100%
height 30px
padding-left 25px
padding-right 10px
line-height 30px
cursor pointer
font-size 14px
border none
overflow ellipsis
text-align left
background-color transparent
color $ui-inactive-text-color
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-info-path
font-size 10px
margin 0 5px
.folderList-item
display block
width 100%
height 3 0px
background-color transparent
color $ui-inactive-text-color
padding 0
margin 2px 0
text-align left
border none
font-size 14px
&:hover
background-color $ui-button--hover-backgroundColor
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
&:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item-name
display block
padding 0 10px
height 30px
line-height 30px
border-width 0 0 0 4px
border-style solid
border-color transparent

View File

@@ -0,0 +1,116 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal'
import ConfigManager from 'browser/main/lib/ConfigManager'
import StorageItem from './StorageItem'
const electron = require('electron')
const { remote } = electron
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
handleMenuButtonClick (e) {
openModal(PreferencesModal)
}
handleHomeButtonClick (e) {
let { router } = this.context
router.push('/home')
}
handleStarredButtonClick (e) {
let { router } = this.context
router.push('/starred')
}
handleToggleButtonClick (e) {
let { dispatch, config } = this.props
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
dispatch({
type: 'SET_IS_SIDENAV_FOLDED',
isFolded: !config.isSideNavFolded
})
}
render () {
let { storages, location, config } = this.props
let isFolded = config.isSideNavFolded
let isHomeActive = location.pathname.match(/^\/home$/)
let isStarredActive = location.pathname.match(/^\/starred$/)
let storageList = storages.map((storage) => {
return <StorageItem
key={storage.key}
storage={storage}
location={location}
/>
})
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
tabIndex='1'
>
<div styleName='top'>
<button styleName='top-menu'
onClick={(e) => this.handleMenuButtonClick(e)}
>
<i className='fa fa-navicon fa-fw'/>
<span styleName='top-menu-label'>Menu</span>
</button>
</div>
<div styleName='menu'>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleHomeButtonClick(e)}
>
<i className='fa fa-home fa-fw'/>
<span styleName='menu-button-label'>Home</span>
</button>
<button styleName={isStarredActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleStarredButtonClick(e)}
>
<i className='fa fa-star fa-fw'/>
<span styleName='menu-button-label'>Starred</span>
</button>
</div>
<div styleName='storageList'>
{storageList.length > 0 ? storageList : (
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>
{false &&
<button styleName='navToggle'
onClick={(e) => this.handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right'/>
: <i className='fa fa-angle-double-left'/>
}
</button>
}
</div>
)
}
}
SideNav.contextTypes = {
router: PropTypes.shape({})
}
SideNav.propTypes = {
dispatch: PropTypes.func,
storages: PropTypes.array,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
}),
location: PropTypes.shape({
pathname: PropTypes.string
})
}
export default CSSModules(SideNav, styles)

View File

@@ -1,104 +0,0 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
import linkState from 'boost/linkState'
import openExternal from 'boost/openExternal'
import { signup } from 'boost/api'
import auth from 'boost/auth'
export default class SignupContainer extends React.Component {
constructor (props) {
super(props)
this.state = {
user: {},
connectionFailed: false,
emailConflicted: false,
nameConflicted: false,
validationFailed: false,
isSending: false,
error: null
}
this.linkState = linkState
this.openExternal = openExternal
}
handleSubmit (e) {
this.setState({
isSending: true,
error: null
}, function () {
signup(this.state.user)
.then(res => {
let { user, token } = res.body
auth.user(user, token)
this.props.history.pushState('home')
})
.catch(err => {
console.error(err)
if (err.code === 'ECONNREFUSED') {
return this.setState({
error: {
name: 'CunnectionRefused',
message: 'Can\'t connect to API server.'
},
isSending: false
})
} else if (err.status != null) {
return this.setState({
error: {
name: err.response.body.name,
message: err.response.body.message
},
isSending: false
})
}
else throw err
})
})
e.preventDefault()
}
render () {
return (
<div className='SignupContainer'>
<img className='logo' src='../../resources/favicon-230x230.png'/>
<nav className='authNavigator text-center'><Link to='/login' activeClassName='active'>Log In</Link> / <Link to='/signup' activeClassName='active'>Sign Up</Link></nav>
<form onSubmit={e => this.handleSubmit(e)}>
<div className='formField'>
<input valueLink={this.linkState('user.email')} type='text' placeholder='E-mail'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.password')} type='password' placeholder='Password'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.name')} type='text' placeholder='name'/>
</div>
<div className='formField'>
<input valueLink={this.linkState('user.profileName')} type='text' placeholder='Profile name'/>
</div>
{this.state.isSending ? (
<p className='alertInfo'>Signing up...</p>
) : null}
{this.state.error != null ? <p className='alertError'>{this.state.error.message}</p> : null}
<div className='formField'>
<button className='logInButton' type='submit'>Sign Up</button>
</div>
</form>
<p className='alert'>会員登録することで<a onClick={this.openExternal} href='http://boostio.github.io/regulations.html'>当サイトの利用規約</a>及び<a onClick={this.openExternal} href='http://boostio.github.io/privacypolicies.html'>Cookieの使用を含むデータに関するポリシー</a></p>
</div>
)
}
}
SignupContainer.propTypes = {
history: PropTypes.shape({
pushState: PropTypes.func
})
}

View File

@@ -0,0 +1,38 @@
.root
absolute bottom left right
height $statusBar-height - 1
background-color $ui-backgroundColor
.pathname
absolute left
height 24px
overflow ellipsis
right 185px
line-height 24px
font-size 12px
padding 0 15px
color $ui-inactive-text-color
.zoom
navButtonColor()
absolute right
height 24px
width 60px
border-width 0 1px
border-style solid
border-color $ui-borderColor
.update
navButtonColor()
position absolute
right 60px
height 24px
width 125px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
.update-icon
color $brand-color

View File

@@ -0,0 +1,94 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
const electron = require('electron')
const ipc = electron.ipcRenderer
const { remote } = electron
const { Menu, MenuItem, dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3]
class StatusBar extends React.Component {
constructor (props) {
super(props)
this.state = {
updateAvailable: false
}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
})
if (index === 0) {
ipc.send('update-app', 'Deal with it.')
}
}
handleZoomButtonClick (e) {
let menu = new Menu()
zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({
label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom)
}))
})
menu.popup(remote.getCurrentWindow())
}
handleZoomMenuItemClick (zoomFactor) {
let { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
type: 'SET_ZOOM',
zoom: zoomFactor
})
}
render () {
let { config, location } = this.props
return (
<div className='StatusBar'
styleName='root'
>
<div styleName='pathname'>{location.pathname + location.search}</div>
{this.state.updateAvailable
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download'/> Update is available!
</button>
: null
}
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<i className='fa fa-search-plus'/>&nbsp;
{Math.floor(config.zoom * 100)}%
</button>
</div>
)
}
}
StatusBar.propTypes = {
config: PropTypes.shape({
zoom: PropTypes.number
})
}
export default CSSModules(StatusBar, styles)

View File

@@ -0,0 +1,106 @@
.root
position relative
width 100%
background-color $ui-backgroundColor
height $topBar-height - 1
$control-height = 34px
.control
position absolute
top 8px
left 8px
right 8px
height $control-height
border $ui-border
border-radius 20px
overflow hidden
.control-search
absolute top left bottom
right 40px
background-color white
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-inactive-text-color
.control-search-input
display block
absolute top bottom right
left 30px
input
width 100%
height 100%
outline none
border none
.control-search-optionList
position fixed
z-index 200
width 275px
height 175px
overflow-y auto
background-color $modal-background
border-radius 2px
box-shadow 2px 2px 10px gray
.control-search-optionList-item
height 50px
border-bottom $ui-border
transition background-color 0.15s
padding 5px
cursor pointer
overflow ellipsis
&:hover
background-color alpha($ui-active-color, 10%)
.control-search-optionList-item-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
overflow ellipsis
font-size 12px
height 16px
margin-bottom 4px
.control-search-optionList-item-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.control-search-optionList-item-type
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.control-search-optionList-empty
height 150px
color $ui-inactive-text-color
line-height 150px
text-align center
.control-newPostButton
display block
absolute top right bottom
width 40px
height $control-height - 2
navButtonColor()
border-left $ui-border
font-size 14px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .left-control-newPostButton-tooltip
display block
.control-newPostButton-tooltip
position fixed
line-height 1.4
background-color $ui-tooltip-backgroundColor
color $ui-tooltip-text-color
font-size 10px
margin-left -25px
margin-top 5px
padding 5px
z-index 1
border-radius 5px
display none
pointer-events none

View File

@@ -0,0 +1,216 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TopBar.styl'
import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
const OSX = window.process.platform === 'darwin'
class TopBar extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
searchOptions: [],
searchPopupOpen: false
}
this.newNoteHandler = () => {
this.handleNewPostButtonClick()
}
}
componentDidMount () {
ee.on('top:new-note', this.newNoteHandler)
}
componentWillUnmount () {
ee.off('top:new-note', this.newNoteHandler)
}
handleNewPostButtonClick (e) {
let { storages, params, dispatch, location } = this.props
let storage = _.find(storages, {key: params.storageKey})
if (storage == null) storage = storages[0]
if (storage == null) throw new Error('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0]
if (folder == null) throw new Error('No folder to craete a note')
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location
})
}
handleSearchChange (e) {
this.setState({
search: this.refs.searchInput.value
})
}
getOptions () {
let { notes } = this.props
let { search } = this.state
if (search.trim().length === 0) return []
let searchBlocks = search.split(' ')
searchBlocks.forEach((block) => {
if (block.match(/^#.+/)) {
let tag = block.match(/#(.+)/)[1]
notes = notes
.filter((note) => {
if (!_.isArray(note.tags)) return false
return note.tags.some((_tag) => {
return _tag === tag
})
})
} else {
notes = notes.filter((note) => {
if (note.type === 'SNIPPET_NOTE') {
return note.description.match(block)
} else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(block)
}
return false
})
}
})
return notes
}
handleOptionClick (uniqueKey) {
return (e) => {
this.setState({
searchPopupOpen: false
}, () => {
let { location } = this.props
hashHistory.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
})
}
}
handleSearchFocus (e) {
this.setState({
searchPopupOpen: true
})
}
handleSearchBlur (e) {
e.stopPropagation()
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.search) {
isStillFocused = true
break
}
el = el.parentNode
}
if (!isStillFocused) {
this.setState({
searchPopupOpen: false
})
}
}
render () {
let { config, style, storages } = this.props
let searchOptionList = this.getOptions()
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
return <div styleName='control-search-optionList-item'
key={note.uniqueKey}
onClick={(e) => this.handleOptionClick(note.uniqueKey)(e)}
>
<div styleName='control-search-optionList-item-folder'
style={{borderColor: folder.color}}>
{folder.name}
<span styleName='control-search-optionList-item-folder-surfix'>in {storage.name}</span>
</div>
{note.type === 'SNIPPET_NOTE'
? <i styleName='control-search-optionList-item-type' className='fa fa-code'/>
: <i styleName='control-search-optionList-item-type' className='fa fa-file-text-o'/>
}&nbsp;
{note.title}
</div>
})
return (
<div className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<div styleName='control-search'>
<i styleName='control-search-icon' className='fa fa-search fa-fw'/>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
tabIndex='-1'
ref='search'
>
<input
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
placeholder='Search'
type='text'
/>
{this.state.searchPopupOpen &&
<div styleName='control-search-optionList'>
{searchOptionList.length > 0
? searchOptionList
: <div styleName='control-search-optionList-empty'>Empty List</div>
}
</div>
}
</div>
{this.state.search > 0 &&
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
<button styleName='control-newPostButton'
onClick={(e) => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus'/>
<span styleName='control-newPostButton-tooltip'>
New Note {OSX ? '⌘' : '^'} + n
</span>
</button>
</div>
</div>
)
}
}
TopBar.contextTypes = {
router: PropTypes.shape({
push: PropTypes.func
})
}
TopBar.propTypes = {
dispatch: PropTypes.func,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
})
}
export default CSSModules(TopBar, styles)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

85
browser/main/global.styl Normal file
View File

@@ -0,0 +1,85 @@
global-reset()
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
html, body
width 100%
height 100%
overflow hidden
body
font-family DEFAULT_FONTS
color textColor
font-size fontSize
font-weight 400
button, input, select, textarea
font-family DEFAULT_FONTS
div, span, a, button, input, textarea
box-sizing border-box
a
color $brand-color
&:hover
color lighten($brand-color, 5%)
&:visited
color $brand-color
hr
border-top none
border-bottom solid 1px $border-color
margin 15px 0
button
font-weight 400
cursor pointer
font-size 12px
&:focus, &.focus
outline none
&:disabled
cursor not-allowed
input
&:disabled
cursor not-allowed
.noSelect
noSelect()
.text-center
text-align center
.form-group
margin-bottom 15px
&>label
display block
margin-bottom 5px
textarea.block-input
resize vertical
height 125px
border-radius 5px
padding 5px 10px
#content
fullsize()
modalZIndex= 1000
modalBackColor = transparentify(white, 65%)
.ace_focus
outline-color rgb(59, 153, 252)
outline-offset 0px
outline-style auto
outline-width 5px
.ModalBase
fixed top left bottom right
z-index modalZIndex
display flex
align-items center
justify-content center
&.hide
display none
.modalBack
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link rel="stylesheet" href="../../node_modules/font-awesome/css/font-awesome.min.css" media="screen" charset="utf-8">
<link rel="stylesheet" href="../../node_modules/devicon/devicon.min.css">
<link rel="shortcut icon" href="favicon.ico">
<style>
@font-face {
font-family: 'Lato';
src: url('../../resources/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../../resources/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../../resources/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
#loadingCover{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
box-sizing: border-box;
padding: 65px 0;
font-family: sans-serif;
}
#loadingCover img{
display: block;
margin: 75px auto 5px;
width: 160px;
height: 160px;
}
#loadingCover .message{
font-size: 30px;
text-align: center;
line-height: 1.6;
font-weight: 100;
color: #888;
}
</style>
</head>
<body>
<div id="loadingCover">
<img src="../../resources/favicon-230x230.png">
<div class='message'>Loading...</div>
</div>
<div id="content"></div>
<script src="../../submodules/ace/src-min/ace.js"></script>
<script type='text/javascript'>
require('web-frame').setZoomLevelLimits(1, 1)
var version = require('remote').require('app').getVersion()
document.title = 'Boost' + ((version == null || version.length === 0) ? ' DEV' : '')
var scriptUrl = process.env.BOOST_ENV === 'development'
? 'http://localhost:8080/assets/main.js'
: '../../compiled/main.js'
var scriptEl=document.createElement('script')
scriptEl.setAttribute("type","text/javascript")
scriptEl.setAttribute("src", scriptUrl)
document.getElementsByTagName("head")[0].appendChild(scriptEl)
</script>
</body>
</html>

View File

@@ -1,44 +1,83 @@
import React from 'react'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
// import { updateUser } from 'boost/actions' import Main from './Main'
import { Router, Route, IndexRoute } from 'react-router' import store from './store'
import MainPage from './MainPage' import React from 'react'
import HomePage from './HomePage'
// import auth from 'boost/auth'
import store from 'boost/store'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
require('../styles/main/index.styl') require('!!style!css!stylus?sourceMap!./global.styl')
import { openModal } from 'boost/modal' import activityRecord from 'browser/lib/activityRecord'
import Tutorial from 'boost/components/modal/Tutorial' import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
import activityRecord from 'boost/activityRecord' import { syncHistoryWithStore } from 'react-router-redux'
import ipc from 'ipc'
const electron = require('electron')
const ipc = electron.ipcRenderer
const path = require('path')
const remote = electron.remote
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
activityRecord.init() activityRecord.init()
window.addEventListener('online', function () { window.addEventListener('online', function () {
ipc.send('check-update', 'check-update') ipc.send('check-update', 'check-update')
}) })
let routes = ( document.addEventListener('drop', function (e) {
<Route path='/' component={MainPage}> e.preventDefault()
<IndexRoute name='home' component={HomePage}/> e.stopPropagation()
</Route> })
) document.addEventListener('dragover', function (e) {
e.preventDefault()
e.stopPropagation()
})
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
}
console.log(options)
return new window.Notification(title, options)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
ipc.on('copy-finder', function () {
activityRecord.emit('FINDER_COPY')
})
ipc.on('open-finder', function () {
activityRecord.emit('FINDER_OPEN')
})
let el = document.getElementById('content') let el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
ReactDOM.render(( ReactDOM.render((
<div> <Provider store={store}>
<Provider store={store}> <Router history={history}>
<Router>{routes}</Router> <Route path='/' component={Main}>
</Provider> <IndexRedirect to='/home'/>
</div> <Route path='home'/>
<Route path='starred'/>
<Route path='storages'>
<IndexRedirect to='/home'/>
<Route path=':storageKey'>
<IndexRoute/>
<Route path='folders/:folderKey'/>
</Route>
</Route>
</Route>
</Router>
</Provider>
), el, function () { ), el, function () {
let loadingCover = document.getElementById('loadingCover') let loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover) loadingCover.parentNode.removeChild(loadingCover)
let status = JSON.parse(localStorage.getItem('status'))
if (status == null) status = {}
if (!status.introWatched) {
openModal(Tutorial)
status.introWatched = true
localStorage.setItem('status', JSON.stringify(status))
}
}) })

View File

@@ -0,0 +1,31 @@
let callees = []
function bind (name, el) {
callees.push({
name: name,
element: el
})
}
function release (el) {
callees = callees.filter((callee) => callee.element !== el)
}
function fire (command) {
console.info('COMMAND >>', command)
let splitted = command.split(':')
let target = splitted[0]
let targetCommand = splitted[1]
let targetCallees = callees
.filter((callee) => callee.name === target)
targetCallees.forEach((callee) => {
callee.element.fire(targetCommand)
})
}
export default {
bind,
release,
fire
}

View File

@@ -0,0 +1,84 @@
import _ from 'lodash'
const OSX = global.process.platform === 'darwin'
const electron = require('electron')
const { ipcRenderer } = electron
const defaultConfig = {
zoom: 1,
isSideNavFolded: false,
listWidth: 250,
hotkey: {
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
},
ui: {
theme: 'default',
disableDirectWrite: false
},
editor: {
theme: 'xcode',
fontSize: '14',
fontFamily: 'Monaco, Consolas',
indentType: 'space',
indentSize: '4',
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
},
preview: {
fontSize: '14',
fontFamily: 'Lato',
codeBlockTheme: 'xcode',
lineNumber: true
}
}
function validate (config) {
if (!_.isObject(config)) return false
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
if (!_.isBoolean(config.isSideNavFolded)) return false
if (!_.isNumber(config.listWidth) || config.listWidth <= 0) return false
return true
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
function get () {
let config = window.localStorage.getItem('config')
try {
config = Object.assign({}, defaultConfig, JSON.parse(config))
if (!validate(config)) throw new Error('INVALID CONFIG')
} catch (err) {
console.warn('Boostnote resets the malformed configuration.')
config = defaultConfig
_save(config)
}
return config
}
function set (updates) {
let currentConfig = get()
let newConfig = Object.assign({}, defaultConfig, currentConfig, updates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: false
})
}
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: true
})
export default {
get,
set,
validate
}

View File

@@ -0,0 +1,30 @@
import ConfigManager from './ConfigManager'
const electron = require('electron')
const { remote } = electron
_init()
function _init () {
setZoom(getZoom(), true)
}
function _saveZoom (zoomFactor) {
ConfigManager.set({zoom: zoomFactor})
}
function setZoom (zoomFactor, noSave = false) {
if (!noSave) _saveZoom(zoomFactor)
remote.getCurrentWebContents().setZoomFactor(zoomFactor)
}
function getZoom () {
let config = ConfigManager.get()
return config.zoom
}
export default {
setZoom,
getZoom
}

565
browser/main/lib/dataApi.js Normal file
View File

@@ -0,0 +1,565 @@
const keygen = require('browser/lib/keygen')
const CSON = require('season')
const path = require('path')
const _ = require('lodash')
const sander = require('sander')
const consts = require('browser/lib/consts')
let storages = []
let notes = []
let queuedTasks = []
function queueSaveFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Failed to queue: Storage doesn\'t exist.')
let targetTasks = queuedTasks.filter((task) => task.storage === storageKey && task.folder === folderKey)
targetTasks.forEach((task) => {
clearTimeout(task.timer)
})
queuedTasks = queuedTasks.filter((task) => task.storage !== storageKey || task.folder !== folderKey)
let newTimer = setTimeout(() => {
let folderNotes = notes.filter((note) => note.storage === storageKey && note.folder === folderKey)
sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
}, 1500)
queuedTasks.push({
storage: storageKey,
folder: folderKey,
timer: newTimer
})
}
class Storage {
constructor (cache) {
this.key = cache.key
this.cache = cache
}
loadJSONData () {
return new Promise((resolve, reject) => {
try {
let data = CSON.readFileSync(path.join(this.cache.path, 'boostnote.json'))
this.data = data
resolve(this)
} catch (err) {
reject(err)
}
})
}
toJSON () {
return Object.assign({}, this.cache, this.data)
}
initStorage () {
return this.loadJSONData()
.catch((err) => {
console.error(err.code)
if (err.code === 'ENOENT') {
let initialStorage = {
folders: []
}
return sander.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(initialStorage))
} else throw err
})
.then(() => this.loadJSONData())
}
saveData () {
return sander
.writeFile(path.join(this.cache.path, 'boostnote.json'), JSON.stringify(this.data))
.then(() => this)
}
saveCache () {
_saveCaches()
}
static forge (cache) {
let instance = new this(cache)
return instance
}
}
class Note {
constructor (note) {
this.storage = note.storage
this.folder = note.folder
this.key = note.key
this.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
this.data = note
}
toJSON () {
return Object.assign({}, this.data, {
uniqueKey: this.uniqueKey
})
}
save () {
let storage = _.find(storages, {key: this.storage})
if (storage == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
let folder = _.find(storage.data.folders, {key: this.folder})
if (folder == null) return Promise.reject(new Error('Storage doesn\'t exist.'))
// FS MUST BE MANIPULATED BY ASYNC METHOD
queueSaveFolder(storage.key, folder.key)
return Promise.resolve(this)
}
static forge (note) {
let instance = new this(note)
return Promise.resolve(instance)
}
}
function init () {
let fetchStorages = function () {
let caches
try {
caches = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(caches)) throw new Error('Cached data is not valid.')
} catch (e) {
console.error(e)
caches = []
localStorage.setItem('storages', JSON.stringify(caches))
}
return caches.map((cache) => {
return Storage
.forge(cache)
.loadJSONData()
.catch((err) => {
console.error(err)
console.error('Failed to load a storage JSON File: %s', cache)
return null
})
})
}
let fetchNotes = function (storages) {
let notes = []
let modifiedStorages = []
storages
.forEach((storage) => {
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
if (modifiedStorages.some((modified) => modified.key === storage.key)) modifiedStorages.push(storage)
return
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
notes.push(Note.forge(note))
})
})
}, [])
return Promise
.all(modifiedStorages.map((storage) => storage.saveData()))
.then(() => Promise.all(notes))
}
return Promise.all(fetchStorages())
.then((_storages) => {
storages = _storages.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
_saveCaches()
return storages
})
.then(fetchNotes)
.then((_notes) => {
notes = _notes
return {
storages: storages.map((storage) => storage.toJSON()),
notes: notes.map((note) => note.toJSON())
}
})
}
function _saveCaches () {
localStorage.setItem('storages', JSON.stringify(storages.map((storage) => storage.cache)))
}
function addStorage (input) {
if (!_.isString(input.path)) {
return Promise.reject(new Error('Path must be a string.'))
}
let key = keygen()
while (storages.some((storage) => storage.key === key)) {
key = keygen()
}
return Storage
.forge({
name: input.name,
key: key,
type: input.type,
path: input.path
})
.initStorage()
.then((storage) => {
let _notes = []
let isFolderRemoved = false
storage.data.folders.forEach((folder) => {
let dataPath = path.join(storage.cache.path, folder.key, 'data.json')
let data
try {
data = CSON.readFileSync(dataPath)
} catch (e) {
// Remove folder if fetching failed.
console.error('Failed to load data: %s', dataPath)
storage.data.folders = storage.data.folders.filter((_folder) => _folder.key !== folder.key)
isFolderRemoved = true
return true
}
data.notes.forEach((note) => {
note.storage = storage.key
note.folder = folder.key
_notes.push(Note.forge(note))
})
})
return Promise.all(_notes)
.then((_notes) => {
notes = notes.concat(_notes)
let data = {
storage: storage,
notes: _notes
}
return isFolderRemoved
? storage.saveData().then(() => data)
: data
})
})
.then((data) => {
storages = storages.filter((storage) => storage.key !== data.storage.key)
storages.push(data.storage)
_saveCaches()
if (data.storage.data.folders.length < 1) {
return createFolder(data.storage.key, {
name: 'Default',
color: consts.FOLDER_COLORS[0]
}).then(() => data)
}
return data
})
.then((data) => {
return {
storage: data.storage.toJSON(),
notes: data.notes.map((note) => note.toJSON())
}
})
}
function removeStorage (key) {
storages = storages.filter((storage) => storage.key !== key)
_saveCaches()
notes = notes.filter((note) => note.storage !== key)
return Promise.resolve(true)
}
function renameStorage (key, name) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.cache.name = name
storage.saveCache()
return Promise.resolve(storage.toJSON())
}
function migrateFromV5 (key, data) {
let oldFolders = data.folders
let oldArticles = data.articles
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let migrateFolders = oldFolders.map((oldFolder) => {
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.data.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
let folderNotes = []
articles.forEach((article) => {
let noteKey = keygen()
while (notes.some((note) => note.storage === key && note.folder === folderKey && note.key === noteKey)) {
key = keygen()
}
if (article.mode === 'markdown') {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
})
notes.push(newNote)
folderNotes.push(newNote)
} else {
let newNote = new Note({
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
}]
})
notes.push(newNote)
folderNotes.push(newNote)
}
})
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify({
notes: folderNotes.map((note) => {
let json = note.toJSON()
delete json.storage
return json
})
}))
})
return Promise.all(migrateFolders)
.then(() => storage.saveData())
.then(() => {
return {
storage: storage.toJSON(),
notes: notes.filter((note) => note.storage === key)
.map((note) => note.toJSON())
}
})
}
function createFolder (key, input) {
let storage = _.find(storages, {key: key})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folderKey = keygen()
while (storage.data.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: input.name,
color: input.color
}
const defaultData = {notes: []}
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.writeFile(path.join(storage.cache.path, folderKey, 'data.json'), JSON.stringify(defaultData))
.then(() => {
storage.data.folders.push(newFolder)
return storage
.saveData()
.then((storage) => storage.toJSON())
})
}
function updateFolder (storageKey, folderKey, input) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: folderKey})
folder.color = input.color
folder.name = input.name
return storage
.saveData()
.then((storage) => storage.toJSON())
}
function removeFolder (storageKey, folderKey) {
let storage = _.find(storages, {key: storageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
storage.data.folders = storage.data.folders.filter((folder) => folder.key !== folderKey)
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey)
// FS MUST BE MANIPULATED BY ASYNC METHOD
return sander
.rimraf(path.join(storage.cache.path, folderKey))
.catch((err) => {
if (err.code === 'ENOENT') return true
else throw err
})
.then(() => storage.saveData())
.then((storage) => storage.toJSON())
}
function createMarkdownNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
content: ''
}, input, {
type: 'MARKDOWN_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function createSnippetNote (storageKey, folderKey, input) {
let key = keygen()
while (notes.some((note) => note.storage === storageKey && note.folder === folderKey && note.key === key)) {
key = keygen()
}
let newNote = new Note(Object.assign({
tags: [],
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}, input, {
type: 'SNIPPET_NOTE',
storage: storageKey,
folder: folderKey,
key: key,
isStarred: false,
createdAt: new Date(),
updatedAt: new Date()
}))
notes.push(newNote)
return newNote
.save()
.then(() => newNote.toJSON())
}
function updateNote (storageKey, folderKey, noteKey, input) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
switch (note.data.type) {
case 'MARKDOWN_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.content = input.content
note.data.updatedAt = input.updatedAt
break
case 'SNIPPET_NOTE':
note.data.title = input.title
note.data.tags = input.tags
note.data.description = input.description
note.data.snippets = input.snippets
note.data.updatedAt = input.updatedAt
}
return note.save()
.then(() => note.toJSON())
}
function removeNote (storageKey, folderKey, noteKey) {
notes = notes.filter((note) => note.storage !== storageKey || note.folder !== folderKey || note.key !== noteKey)
queueSaveFolder(storageKey, folderKey)
return Promise.resolve(null)
}
function moveNote (storageKey, folderKey, noteKey, newStorageKey, newFolderKey) {
let note = _.find(notes, {
key: noteKey,
storage: storageKey,
folder: folderKey
})
if (note == null) throw new Error('Note doesn\'t exist.')
let storage = _.find(storages, {key: newStorageKey})
if (storage == null) throw new Error('Storage doesn\'t exist.')
let folder = _.find(storage.data.folders, {key: newFolderKey})
if (folder == null) throw new Error('Folder doesn\'t exist.')
note.storage = storage.key
note.data.storage = storage.key
note.folder = folder.key
note.data.folder = folder.key
let key = note.key
while (notes.some((note) => note.storage === storage.key && note.folder === folder.key && note.key === key)) {
key = keygen()
}
note.key = key
note.data.key = key
note.uniqueKey = `${note.storage}-${note.folder}-${note.key}`
console.log(note.uniqueKey)
queueSaveFolder(storageKey, folderKey)
return note.save()
.then(() => note.toJSON())
}
export default {
init,
addStorage,
removeStorage,
renameStorage,
createFolder,
updateFolder,
removeFolder,
createMarkdownNote,
createSnippetNote,
updateNote,
removeNote,
moveNote,
migrateFromV5
}

View File

@@ -0,0 +1,26 @@
const electron = require('electron')
const { ipcRenderer, remote } = electron
function on (name, listener) {
ipcRenderer.on(name, listener)
}
function off (name, listener) {
ipcRenderer.removeListener(name, listener)
}
function once (name, listener) {
ipcRenderer.once(name, listener)
}
function emit (name, ...args) {
console.log(name)
remote.getCurrentWindow().webContents.send(name, ...args)
}
export default {
emit,
on,
off,
once
}

View File

@@ -1,5 +1,7 @@
import React from 'react' import React from 'react'
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import store from '../store'
class ModalBase extends React.Component { class ModalBase extends React.Component {
constructor (props) { constructor (props) {
@@ -18,9 +20,11 @@ class ModalBase extends React.Component {
render () { render () {
return ( return (
<div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}> <div className={'ModalBase' + (this.state.isHidden ? ' hide' : '')}>
<div onClick={e => this.close(e)} className='modalBack'/> <div onClick={(e) => this.close(e)} className='modalBack'/>
{this.state.component == null ? null : ( {this.state.component == null ? null : (
<this.state.component {...this.state.componentProps} close={this.close}/> <Provider store={store}>
<this.state.component {...this.state.componentProps} close={this.close}/>
</Provider>
)} )}
</div> </div>
) )
@@ -33,14 +37,22 @@ let modalBase = ReactDOM.render(<ModalBase/>, el)
export function openModal (component, props) { export function openModal (component, props) {
if (modalBase == null) { return } if (modalBase == null) { return }
document.body.setAttribute('data-modal', 'open')
modalBase.setState({component: component, componentProps: props, isHidden: false}) modalBase.setState({component: component, componentProps: props, isHidden: false})
} }
export function closeModal () { export function closeModal () {
if (modalBase == null) { return } if (modalBase == null) { return }
modalBase.setState({component: null, componentProps: null, isHidden: true}) modalBase.close()
} }
export function isModalOpen () { export function isModalOpen () {
return !modalBase.state.isHidden return !modalBase.state.isHidden
} }
export default {
open: openModal,
close: closeModal,
isOpen: isModalOpen
}

View File

@@ -0,0 +1,53 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import store from '../store'
const electron = require('electron')
const ipc = electron.ipcRenderer
export default class DeleteArticleModal extends React.Component {
constructor (props) {
super(props)
this.confirmHandler = (e) => this.handleYesButtonClick()
}
componentDidMount () {
ReactDOM.findDOMNode(this.refs.no).focus()
ipc.on('modal-confirm', this.confirmHandler)
}
componentWillUnmount () {
ipc.removeListener('modal-confirm', this.confirmHandler)
}
handleNoButtonClick (e) {
this.props.close()
}
handleYesButtonClick (e) {
// store.dispatch(destroyArticle(this.props.articleKey))
this.props.close()
}
render () {
return (
<div className='DeleteArticleModal modal'>
<div className='title'><i className='fa fa-fw fa-trash'/> Delete an article.</div>
<div className='message'>Do you really want to delete?</div>
<div className='control'>
<button ref='no' onClick={(e) => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close'/> No</button>
<button ref='yes' onClick={(e) => this.handleYesButtonClick(e)} className='danger'><i className='fa fa-fw fa-check'/> Yes</button>
</div>
</div>
)
}
}
DeleteArticleModal.propTypes = {
action: PropTypes.object,
articleKey: PropTypes.string,
close: PropTypes.func
}

View File

@@ -0,0 +1,243 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InitModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { hashHistory } from 'react-router'
import _ from 'lodash'
const CSON = require('season')
const path = require('path')
const electron = require('electron')
const { remote } = electron
function browseFolder () {
let dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
defaultPath,
properties: ['openDirectory', 'createDirectory']
}, function (targetPaths) {
if (targetPaths == null) return resolve('')
resolve(targetPaths[0])
})
})
}
class InitModal extends React.Component {
constructor (props) {
super(props)
this.state = {
path: path.join(remote.app.getPath('home'), 'Boostnote'),
migrationRequested: true,
isLoading: true,
data: null,
legacyStorageExists: false,
isSending: false
}
}
handleCloseButtonClick (e) {
this.props.close()
}
handlePathChange (e) {
this.setState({
path: e.target.value
})
}
componentDidMount () {
let data = null
try {
data = CSON.readFileSync(path.join(remote.app.getPath('userData'), 'local.json'))
} catch (err) {
console.error(err)
}
let newState = {
isLoading: false
}
if (data != null) {
newState.legacyStorageExists = true
newState.data = data
}
this.setState(newState, () => {
this.refs.createButton.focus()
})
}
handlePathBrowseButtonClick (e) {
browseFolder()
.then((targetPath) => {
if (targetPath.length > 0) {
this.setState({
path: targetPath
})
}
})
.catch((err) => {
console.error('BrowseFAILED')
console.error(err)
})
}
handleSubmitButtonClick (e) {
this.setState({
isSending: true
}, () => {
dataApi
.addStorage({
name: 'My Storage',
path: this.state.path
})
.then((data) => {
if (this.state.migrationRequested && _.isObject(this.state.data) && _.isArray(this.state.data.folders) && _.isArray(this.state.data.articles)) {
return dataApi.migrateFromV5(data.storage.key, this.state.data)
}
return data
})
.then((data) => {
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
notes: data.notes
})
let defaultMarkdownNote = dataApi
.createMarkdownNote(data.storage.key, data.storage.folders[0].key, {
title: 'Welcome to Boostnote :)',
content: '# Welcome to Boostnote :)\nThis is a markdown note.\n\nClick to edit this note.'
})
.then((note) => {
store.dispatch({
type: 'CREATE_NOTE',
note: note
})
})
let defaultSnippetNote = dataApi
.createSnippetNote(data.storage.key, data.storage.folders[0].key, {
title: 'Snippet note example',
description: 'Snippet note example\nYou can store a series of snippet as a single note like Gist.',
snippets: [
{
name: 'example.html',
mode: 'html',
content: '<html>\n<body>\n<h1 id=\'hello\'>Hello World</h1>\n</body>\n</html>'
},
{
name: 'example.js',
mode: 'javascript',
content: 'var html = document.getElementById(\'hello\').innerHTML\n\nconsole.log(html)'
}
]
})
.then((note) => {
store.dispatch({
type: 'CREATE_NOTE',
note: note
})
})
return Promise.resolve(defaultSnippetNote)
.then(defaultMarkdownNote)
.then(() => data.storage)
})
.then((storage) => {
hashHistory.push('/storages/' + storage.key)
this.props.close()
})
.catch((err) => {
this.setState({
isSending: false
})
throw err
})
})
}
handleMigrationRequestedChange (e) {
this.setState({
migrationRequested: e.target.checked
})
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
if (this.state.isLoading) {
return <div styleName='root--loading'>
<i styleName='spinner' className='fa fa-spin fa-spinner'/>
<div styleName='loadingMessage'>Preparing initialization...</div>
</div>
}
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='header-title'>Initialize Storage</div>
</div>
<button styleName='closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>Close</button>
<div styleName='body'>
<div styleName='body-welcome'>
Welcome you!
</div>
<div styleName='body-description'>
Boostnote will use this directory as a default storage.
</div>
<div styleName='body-path'>
<input styleName='body-path-input'
placeholder='Select Folder'
value={this.state.path}
onChange={(e) => this.handlePathChange(e)}
/>
<button styleName='body-path-button'
onClick={(e) => this.handlePathBrowseButtonClick(e)}
>
...
</button>
</div>
{this.state.legacyStorageExists &&
<div styleName='body-migration'>
<label><input type='checkbox' checked={this.state.migrationRequested} onChange={(e) => this.handleMigrationRequestedChange(e)}/> Migrate old data from the legacy app v0.5</label>
</div>
}
<div styleName='body-control'>
<button styleName='body-control-createButton'
ref='createButton'
onClick={(e) => this.handleSubmitButtonClick(e)}
disabled={this.state.isSending}
>
{this.state.isSending
? <span>
<i className='fa fa-spin fa-spinner'/> Loading...
</span>
: 'Let\'s Go!'
}
</button>
</div>
</div>
</div>
)
}
}
InitModal.propTypes = {
}
export default CSSModules(InitModal, styles)

View File

@@ -0,0 +1,86 @@
.root
modal()
max-width 540px
overflow hidden
position relative
.root--loading
@extend .root
text-align center
.spinner
font-size 100px
margin 35px auto
color $ui-text-color
.loadingMessage
color $ui-text-color
margin 15px auto 35px
.header
height 50px
font-size 18px
line-height 50px
padding 0 15px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
color $ui-text-color
.closeButton
position absolute
top 10px
right 10px
height 30px
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.body
padding 30px
.body-welcome
text-align center
margin-bottom 25px
font-size 32px
color $ui-text-color
.body-description
font-size 14px
color $ui-text-color
text-align center
margin-bottom 25px
.body-path
margin 0 auto 25px
width 280px
.body-path-input
height 30px
vertical-align middle
width 250px
font-size 12px
border-style solid
border-width 1px 0 1px 1px
border-color $border-color
border-top-left-radius 2px
border-bottom-left-radius 2px
padding 0 5px
.body-path-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
.body-migration
margin 0 auto 25px
text-align center
.body-control
text-align center
.body-control-createButton
colorPrimaryButton()
border none
border-radius 2px
height 40px
padding 0 25px

View File

@@ -0,0 +1,140 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
class NewNoteModal extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
componentDidMount () {
this.refs.markdownButton.focus()
}
handleCloseButtonClick (e) {
this.props.close()
}
handleMarkdownNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props
dataApi
.createMarkdownNote(storage, folder, {
title: '',
content: ''
})
.then((note) => {
dispatch({
type: 'CREATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.uniqueKey}
})
ee.emit('detail:focus')
this.props.close()
})
}
handleMarkdownNoteButtonKeyDown (e) {
if (e.keyCode === 9) {
e.preventDefault()
this.refs.snippetButton.focus()
}
}
handleSnippetNoteButtonClick (e) {
let { storage, folder, dispatch, location } = this.props
dataApi
.createSnippetNote(storage, folder, {
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
})
.then((note) => {
dispatch({
type: 'CREATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.uniqueKey}
})
ee.emit('detail:focus')
this.props.close()
})
}
handleSnippetNoteButtonKeyDown (e) {
if (e.keyCode === 9) {
e.preventDefault()
this.refs.markdownButton.focus()
}
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>New Note</div>
</div>
<button styleName='closeButton'
onClick={(e) => this.handleCloseButtonClick(e)}
>Close</button>
<div styleName='control'>
<button styleName='control-button'
onClick={(e) => this.handleMarkdownNoteButtonClick(e)}
onKeyDown={(e) => this.handleMarkdownNoteButtonKeyDown(e)}
ref='markdownButton'
>
<i styleName='control-button-icon'
className='fa fa-file-text-o'
/><br/>
<span styleName='control-button-label'>Markdown Note</span><br/>
<span styleName='control-button-description'>It is good for any type of documents. Check List, Code block and Latex block are available.</span>
</button>
<button styleName='control-button'
onClick={(e) => this.handleSnippetNoteButtonClick(e)}
onKeyDown={(e) => this.handleSnippetNoteButtonKeyDown(e)}
ref='snippetButton'
>
<i styleName='control-button-icon'
className='fa fa-code'
/><br/>
<span styleName='control-button-label'>Snippet Note</span><br/>
<span styleName='control-button-description'>This format is specialized on managing snippets like Gist. Multiple snippets can be grouped as a note.
</span>
</button>
</div>
<div styleName='description'><i className='fa fa-arrows-h'/> Tab to switch format</div>
</div>
)
}
}
NewNoteModal.propTypes = {
}
export default CSSModules(NewNoteModal, styles)

View File

@@ -0,0 +1,56 @@
.root
modal()
max-width 540px
overflow hidden
position relative
.header
height 50px
font-size 18px
line-height 50px
padding 0 15px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
color $ui-text-color
.closeButton
position absolute
top 10px
right 10px
height 30px
width 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.control
padding 25px 15px 15px
text-align center
.control-button
width 220px
height 220px
margin 0 15px
border $ui-border
border-radius 5px
color $ui-text-color
colorDefaultButton()
padding 10px
&:focus
colorPrimaryButton()
.control-button-icon
font-size 50px
margin-bottom 15px
.control-button-label
font-size 18px
line-height 32px
.control-button-description
font-size 12px
.description
color $ui-inactive-text-color
text-align center
margin-bottom 25px

View File

@@ -0,0 +1,412 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import hljsTheme from 'browser/lib/hljsThemes'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
const electron = require('electron')
const ipc = electron.ipcRenderer
const ace = window.ace
const OSX = global.process.platform === 'darwin'
class ConfigTab extends React.Component {
constructor (props) {
super(props)
this.state = {
isHotkeyHintOpen: false,
config: props.config
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully applied!'
}})
}
this.handleSettingError = (err) => {
this.setState({keymapAlert: {
type: 'error',
message: err.message != null ? err.message : 'Error occurs!'
}})
}
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.addListener('APP_SETTING_ERROR', this.handleSettingError)
}
componentWillUnmount () {
ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone)
ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError)
}
handleSaveButtonClick (e) {
let newConfig = {
hotkey: this.state.config.hotkey
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
handleConfigKeyDown (e) {
if (e.keyCode === 13) {
this.submitConfig()
}
}
handleLineNumberingClick (e) {
let config = this.state.config
config['preview-line-number'] = e.target.checked
this.setState({
config
})
}
handleDisableDirectWriteClick (e) {
let config = this.state.config
config['disable-direct-write'] = e.target.checked
this.setState({
config
})
}
handleHintToggleButtonClick (e) {
this.setState({
isHotkeyHintOpen: !this.state.isHotkeyHintOpen
})
}
handleHotkeyChange (e) {
let { config } = this.state
config.hotkey = {
toggleFinder: this.refs.toggleFinder.value,
toggleMain: this.refs.toggleMain.value
}
this.setState({
config
})
}
handleUIChange (e) {
let { config } = this.state
config.ui = {
theme: this.refs.uiTheme.value,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
}
config.editor = {
theme: this.refs.editorTheme.value,
fontSize: this.refs.editorFontSize.value,
fontFamily: this.refs.editorFontFamily.value,
indentType: this.refs.editorIndentType.value,
indentSize: this.refs.editorIndentSize.value,
switchPreview: this.refs.editorSwitchPreview.value
}
config.preview = {
fontSize: this.refs.previewFontSize.value,
fontFamily: this.refs.previewFontFamily.value,
codeBlockTheme: this.refs.previewCodeBlockTheme.value,
lineNumber: this.refs.previewLineNumber.checked
}
this.setState({
config
})
}
handleSaveUIClick (e) {
let newConfig = {
ui: this.state.config.ui,
editor: this.state.config.editor,
preview: this.state.config.preview
}
ConfigManager.set(newConfig)
store.dispatch({
type: 'SET_UI',
config: newConfig
})
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
? <p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
: null
let aceThemeList = ace.require('ace/ext/themelist')
let hljsThemeList = hljsTheme()
let { config } = this.state
return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>Hotkey</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Toggle Main</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleMain'
value={config.hotkey.toggleMain}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Toggle Finder(popup)</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleFinder'
value={config.hotkey.toggleFinder}
type='text'
disabled
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}
>
{this.state.isHotkeyHintOpen
? 'Hide Hint'
: 'Show Hint'
}
</button>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveButtonClick(e)}>Save Hotkey
</button>
{keymapAlertElement}
</div>
{this.state.isHotkeyHintOpen &&
<div styleName='group-hint'>
<p>Available Keys</p>
<ul>
<li><code>0</code> to <code>9</code></li>
<li><code>A</code> to <code>Z</code></li>
<li><code>F1</code> to <code>F24</code></li>
<li>Punctuations like <code>~</code>, <code>!</code>, <code>@</code>, <code>#</code>, <code>$</code>, etc.</li>
<li><code>Plus</code></li>
<li><code>Space</code></li>
<li><code>Backspace</code></li>
<li><code>Delete</code></li>
<li><code>Insert</code></li>
<li><code>Return</code> (or <code>Enter</code> as alias)</li>
<li><code>Up</code>, <code>Down</code>, <code>Left</code> and <code>Right</code></li>
<li><code>Home</code> and <code>End</code></li>
<li><code>PageUp</code> and <code>PageDown</code></li>
<li><code>Escape</code> (or <code>Esc</code> for short)</li>
<li><code>VolumeUp</code>, <code>VolumeDown</code> and <code>VolumeMute</code></li>
<li><code>MediaNextTrack</code>, <code>MediaPreviousTrack</code>, <code>MediaStop</code> and <code>MediaPlayPause</code></li>
</ul>
</div>
}
</div>
<div styleName='group'>
<div styleName='group-header'>UI</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Theme</div>
<div styleName='group-section-control'>
<select value={config.ui.theme}
onChange={(e) => this.handleUIChange(e)}
ref='uiTheme'
disabled
>
<option value='default'>Light</option>
<option value='dark'>Dark</option>
</select>
</div>
</div>
{
global.process.platform === 'win32'
? <div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.ui.disableDirectWrite}
refs='uiD2w'
disabled={OSX}
type='checkbox'
/>
Disable Direct Write(It will be applied after restarting)
</label>
</div>
: null
}
<div styleName='group-header2'>Editor</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Theme
</div>
<div styleName='group-section-control'>
<select value={config.editor.theme}
ref='editorTheme'
onChange={(e) => this.handleUIChange(e)}
>
{
aceThemeList.themes.map((theme) => {
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
})
}
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Size
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='editorFontSize'
value={config.editor.fontSize}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Font Family
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='editorFontFamily'
value={config.editor.fontFamily}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Editor Indent Style
</div>
<div styleName='group-section-control'>
<select value={config.editor.indentSize}
ref='editorIndentSize'
onChange={(e) => this.handleUIChange(e)}
>
<option value='1'>1</option>
<option value='2'>2</option>
<option value='4'>4</option>
<option value='8'>8</option>
</select>&nbsp;
<select value={config.editor.indentType}
ref='editorIndentType'
onChange={(e) => this.handleUIChange(e)}
>
<option value='space'>Spaces</option>
<option value='tab'>Tabs</option>
</select>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Switching Preview
</div>
<div styleName='group-section-control'>
<select value={config.editor.switchPreview}
ref='editorSwitchPreview'
onChange={(e) => this.handleUIChange(e)}
>
<option value='BLUR'>When Editor Blurred</option>
<option value='RIGHTCLICK'>When Right Clicking</option>
</select>
</div>
</div>
<div styleName='group-header2'>Preview</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Size
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={config.preview.fontSize}
ref='previewFontSize'
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
Preview Font Family
</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='previewFontFamily'
value={config.preview.fontFamily}
onChange={(e) => this.handleUIChange(e)}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>Code block Theme</div>
<div styleName='group-section-control'>
<select value={config.preview.codeBlockTheme}
ref='previewCodeBlockTheme'
onChange={(e) => this.handleUIChange(e)}
>
{
hljsThemeList.map((theme) => {
return (<option value={theme.name} key={theme.name}>{theme.caption}</option>)
})
}
</select>
</div>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.lineNumber}
ref='previewLineNumber'
type='checkbox'
/>&nbsp;
Code block line numbering
</label>
</div>
<div className='group-control'>
<button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}
>
Save UI Config
</button>
</div>
</div>
</div>
)
}
}
ConfigTab.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
dispatch: PropTypes.func
}
export default CSSModules(ConfigTab, styles)

View File

@@ -0,0 +1,88 @@
.root
padding 15px
color $ui-text-color
.group
margin-bottom 45px
.group-header
font-size 24px
color $ui-text-color
padding 5px
border-bottom $default-border
margin-bottom 15px
.group-header2
font-size 18px
color $ui-text-color
padding 5px
margin-bottom 15px
.group-section
margin-bottom 15px
display flex
line-height 30px
.group-section-label
width 150px
text-align right
margin-right 10px
.group-section-control
flex 1
.group-section-control-input
height 30px
vertical-align middle
width 150px
font-size 12px
border solid 1px $border-color
border-radius 2px
padding 0 5px
&:disabled
background-color $ui-input--disabled-backgroundColor
.group-checkBoxSection
margin-bottom 15px
display flex
line-height 30px
padding-left 15px
.group-control
border-top $default-border
padding-top 10px
box-sizing border-box
height 40px
text-align right
:global
.alert
font-size 12px
line-height 30px
padding 0 5px
float right
.group-control-leftButton
float left
colorDefaultButton()
border $default-border
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.group-control-rightButton
float right
colorPrimaryButton()
border none
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.group-hint
border $ui-border
padding 10px 15px
margin 15px 0
border-radius 5px
background-color $ui-backgroundColor
color $ui-inactive-text-color
ul
list-style inherit
padding-left 1em
line-height 1.2
p
line-height 1.2

View File

@@ -0,0 +1,58 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoTab.styl'
const electron = require('electron')
const { shell, remote } = electron
const appVersion = remote.app.getVersion()
class InfoTab extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
handleLinkClick (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}
render () {
return (
<div styleName='root'>
<div styleName='top'>
<img styleName='icon' src='../resources/app.png' width='150' height='150'/>
<div styleName='appId'>Boostnote {appVersion}</div>
<div styleName='description'>
A simple markdown/snippet note app for developer.
</div>
<div styleName='madeBy'>Made by&nbsp;
<a href='http://maisin.co/'
onClick={(e) => this.handleLinkClick(e)}
>MAISIN&CO.</a></div>
<div styleName='copyright'>Copyright 2016 MAISIN&CO. All rights reserved.</div>
</div>
<ul styleName='list'>
<li>
The codes of this app is published under GPLv3 license.
</li>
<li>
Any kinds of feedback, creating a new issue or a pull request, would be welcomed.
</li>
<li>
Issue Tracker : <a href='https://github.com/BoostIO/Boostnote/issues'
onClick={(e) => this.handleLinkClick(e)}
>https://github.com/BoostIO/Boostnote/issues</a>
</li>
</ul>
</div>
)
}
}
InfoTab.propTypes = {
}
export default CSSModules(InfoTab, styles)

View File

@@ -0,0 +1,35 @@
.root
padding 15px
white-space pre
line-height 1.4
color $ui-text-color
width 100%
.top
text-align center
margin-bottom 25px
.appId
font-size 24px
.description
overflow hidden
white-space normal
line-height 1.5
margin 5px auto 10px
font-size 14px
text-align center
.madeBy
font-size 12px
$ui-inactive-text-color
.copyright
font-size 12px
$ui-inactive-text-color
.list
list-style square
padding-left 2em
li
white-space normal

View File

@@ -0,0 +1,37 @@
.root
modal()
max-width 540px
min-height 400px
height 80%
overflow hidden
position relative
.nav
absolute top left right
height 50px
background-color $ui-backgroundColor
border-bottom solid 1px $ui-borderColor
.nav-button
width 80px
height 50px
border none
background-color transparent
color #939395
font-size 14px
&:hover
color #515151
.nav-button--active
@extend .nav-button
color #6AA5E9
&:hover
color #6AA5E9
.nav-button-icon
display block
.content
absolute left right bottom
top 50px
overflow-y auto

View File

@@ -0,0 +1,351 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import consts from 'browser/lib/consts'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
const electron = require('electron')
const { shell, remote } = electron
const { Menu, MenuItem } = remote
class UnstyledFolderItem extends React.Component {
constructor (props) {
super(props)
this.state = {
status: 'IDLE',
folder: {
color: props.color,
name: props.name
}
}
}
handleEditChange (e) {
let { folder } = this.state
folder.name = this.refs.nameInput.value
this.setState({
folder
})
}
handleConfirmButtonClick (e) {
let { storage, folder } = this.props
dataApi
.updateFolder(storage.key, folder.key, {
color: this.state.folder.color,
name: this.state.folder.name
})
.then((storage) => {
store.dispatch({
type: 'UPDATE_STORAGE',
storage: storage
})
this.setState({
status: 'IDLE'
})
})
}
handleColorButtonClick (e) {
var menu = new Menu()
consts.FOLDER_COLORS.forEach((color, index) => {
menu.append(new MenuItem({
label: consts.FOLDER_COLOR_NAMES[index],
click: (e) => {
let { folder } = this.state
folder.color = color
this.setState({
folder
})
}
}))
})
menu.popup(remote.getCurrentWindow())
}
handleCancelButtonClick (e) {
this.setState({
status: 'IDLE'
})
}
renderEdit (e) {
return (
<div styleName='folderList-item'>
<div styleName='folderList-item-left'>
<button styleName='folderList-item-left-colorButton' style={{color: this.state.folder.color}}
onClick={(e) => this.handleColorButtonClick(e)}
>
<i className='fa fa-square'/>
</button>
<input styleName='folderList-item-left-nameInput'
value={this.state.folder.name}
ref='nameInput'
onChange={(e) => this.handleEditChange(e)}
/>
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleDeleteConfirmButtonClick (e) {
let { storage, folder } = this.props
dataApi
.removeFolder(storage.key, folder.key)
.then((storage) => {
store.dispatch({
type: 'REMOVE_FOLDER',
key: folder.key,
storage: storage
})
})
}
renderDelete () {
return (
<div styleName='folderList-item'>
<div styleName='folderList-item-left'>
Are you sure to <span styleName='folderList-item-left-danger'>delete</span> this folder?
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-dangerButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>
Confirm
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleCancelButtonClick(e)}
>
Cancel
</button>
</div>
</div>
)
}
handleEditButtonClick (e) {
let { folder } = this.props
this.setState({
status: 'EDIT',
folder: {
color: folder.color,
name: folder.name
}
}, () => {
this.refs.nameInput.select()
})
}
handleDeleteButtonClick (e) {
this.setState({
status: 'DELETE'
})
}
renderIdle () {
let { folder } = this.props
return (
<div styleName='folderList-item'
onDoubleClick={(e) => this.handleEditButtonClick(e)}
>
<div styleName='folderList-item-left'
style={{borderColor: folder.color}}
>
<span styleName='folderList-item-left-name'>{folder.name}</span>
<span styleName='folderList-item-left-key'>({folder.key})</span>
</div>
<div styleName='folderList-item-right'>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleEditButtonClick(e)}
>
Edit
</button>
<button styleName='folderList-item-right-button'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
Delete
</button>
</div>
</div>
)
}
render () {
switch (this.state.status) {
case 'DELETE':
return this.renderDelete()
case 'EDIT':
return this.renderEdit()
case 'IDLE':
default:
return this.renderIdle()
}
}
}
const FolderItem = CSSModules(UnstyledFolderItem, styles)
class StorageItem extends React.Component {
constructor (props) {
super(props)
this.state = {
isLabelEditing: false
}
}
handleNewFolderButtonClick (e) {
let { storage } = this.props
let input = {
name: 'Untitled',
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
dataApi.createFolder(storage.key, input)
.then((storage) => {
store.dispatch({
type: 'ADD_FOLDER',
storage: storage
})
})
.catch((err) => {
console.error(err)
})
}
handleExternalButtonClick () {
let { storage } = this.props
shell.showItemInFolder(storage.path)
}
handleUnlinkButtonClick (e) {
let { storage } = this.props
dataApi.removeStorage(storage.key)
.then(() => {
store.dispatch({
type: 'REMOVE_STORAGE',
key: storage.key
})
})
.catch((err) => {
console.error(err)
})
}
handleLabelClick (e) {
let { storage } = this.props
this.setState({
isLabelEditing: true,
name: storage.name
}, () => {
this.refs.label.focus()
})
}
handleLabelChange (e) {
this.setState({
name: this.refs.label.value
})
}
handleLabelBlur (e) {
let { storage } = this.props
dataApi
.renameStorage(storage.key, this.state.name)
.then((storage) => {
store.dispatch({
type: 'RENAME_STORAGE',
storage: storage
})
this.setState({
isLabelEditing: false
})
})
}
render () {
let { storage } = this.props
let folderList = storage.folders.map((folder) => {
return <FolderItem key={folder.key}
folder={folder}
storage={storage}
/>
})
return (
<div styleName='root' key={storage.key}>
<div styleName='header'>
{this.state.isLabelEditing
? <div styleName='header-label--edit'>
<input styleName='header-label-input'
value={this.state.name}
ref='label'
onChange={(e) => this.handleLabelChange(e)}
onBlur={(e) => this.handleLabelBlur(e)}
/>
</div>
: <div styleName='header-label'
onClick={(e) => this.handleLabelClick(e)}
>
<i className='fa fa-folder-open'/>&nbsp;
{storage.name}&nbsp;
<span styleName='header-label-path'>({storage.path})</span>&nbsp;
<i styleName='header-label-editButton' className='fa fa-pencil'/>
</div>
}
<div styleName='header-control'>
<button styleName='header-control-button'
onClick={(e) => this.handleNewFolderButtonClick(e)}
>
<i className='fa fa-plus'/>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleExternalButtonClick(e)}
>
<i className='fa fa-external-link'/>
</button>
<button styleName='header-control-button'
onClick={(e) => this.handleUnlinkButtonClick(e)}
>
<i className='fa fa-unlink'/>
</button>
</div>
</div>
<div styleName='folderList'>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>No Folders</div>
}
</div>
</div>
)
}
}
StorageItem.propTypes = {
storage: PropTypes.shape({
key: PropTypes.string
}),
folder: PropTypes.shape({
key: PropTypes.string,
color: PropTypes.string,
name: PropTypes.string
})
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,119 @@
.root
position relative
margin-bottom 15px
.header
height 35px
line-height 30px
padding 0 10px 5px
box-sizing border-box
border-bottom $default-border
margin-bottom 5px
.header-label
float left
cursor pointer
&:hover
.header-label-editButton
opacity 1
.header-label-path
color $ui-inactive-text-color
font-size 10px
margin 0 5px
.header-label-editButton
color $ui-text-color
opacity 0
transition 0.2s
.header-label--edit
@extend .header-label
.header-label-input
height 25px
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
padding 0 5px
.header-control
float right
.header-control-button
width 30px
height 25px
colorDefaultButton()
border-radius 2px
border $ui-border
margin-right 5px
&:last-child
margin-right 0
.folderList-item
height 35px
box-sizing border-box
padding 2.5px 15px
&:hover
background-color darken(white, 3%)
.folderList-item-left
height 30px
border-left solid 4px transparent
padding 0 10px
line-height 30px
float left
.folderList-item-left-danger
color $danger-color
font-weight bold
.folderList-item-left-key
color $ui-inactive-text-color
font-size 10px
margin 0 5px
border none
.folderList-item-left-colorButton
colorDefaultButton()
height 25px
width 25px
line-height 23px
padding 0
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
margin-right 5px
margin-left -15px
.folderList-item-left-nameInput
height 25px
box-sizing border-box
vertical-align middle
border $ui-border
border-radius 2px
padding 0 5px
.folderList-item-right
float right
.folderList-item-right-button
vertical-align middle
height 25px
margin-top 2.5px
colorDefaultButton()
border-radius 2px
border $ui-border
margin-right 5px
padding 0 5px
&:last-child
margin-right 0
.folderList-item-right-confirmButton
@extend .folderList-item-right-button
border none
colorPrimaryButton()
.folderList-item-right-dangerButton
@extend .folderList-item-right-button
border none
colorDangerButton()

View File

@@ -0,0 +1,223 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi'
import StorageItem from './StorageItem'
const electron = require('electron')
const remote = electron.remote
function browseFolder () {
let dialog = remote.dialog
let defaultPath = remote.app.getPath('home')
return new Promise((resolve, reject) => {
dialog.showOpenDialog({
title: 'Select Directory',
defaultPath,
properties: ['openDirectory', 'createDirectory']
}, function (targetPaths) {
if (targetPaths == null) return resolve('')
resolve(targetPaths[0])
})
})
}
class StoragesTab extends React.Component {
constructor (props) {
super(props)
this.state = {
page: 'LIST',
newStorage: {
name: 'Unnamed',
type: 'FILESYSTEM',
path: ''
}
}
}
handleAddStorageButton (e) {
this.setState({
page: 'ADD_STORAGE',
newStorage: {
name: 'Unnamed',
type: 'FILESYSTEM',
path: ''
}
}, () => {
this.refs.addStorageName.select()
})
}
renderList () {
let { storages } = this.props
let storageList = storages.map((storage) => {
return <StorageItem
key={storage.key}
storage={storage}
/>
})
return (
<div styleName='list'>
{storageList.length > 0
? storageList
: <div styleName='list-empty'>No storage found.</div>
}
<div styleName='list-control'>
<button styleName='list-control-addStorageButton'
onClick={(e) => this.handleAddStorageButton(e)}
>
<i className='fa fa-plus'/> Add Storage
</button>
</div>
</div>
)
}
handleAddStorageBrowseButtonClick (e) {
browseFolder()
.then((targetPath) => {
if (targetPath.length > 0) {
let { newStorage } = this.state
newStorage.path = targetPath
this.setState({
newStorage
})
}
})
.catch((err) => {
console.error('BrowseFAILED')
console.error(err)
})
}
handleAddStorageChange (e) {
let { newStorage } = this.state
newStorage.name = this.refs.addStorageName.value
newStorage.path = this.refs.addStoragePath.value
this.setState({
newStorage
})
}
handleAddStorageCreateButton (e) {
dataApi
.addStorage({
name: this.state.newStorage.name,
path: this.state.newStorage.path
})
.then((data) => {
let { dispatch } = this.props
dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
notes: data.notes
})
this.setState({
page: 'LIST'
})
})
}
handleAddStorageCancelButton (e) {
this.setState({
page: 'LIST'
})
}
renderAddStorage () {
return (
<div styleName='addStorage'>
<div styleName='addStorage-header'>Add Storage</div>
<div styleName='addStorage-body'>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>
Name
</div>
<div styleName='addStorage-body-section-name'>
<input styleName='addStorage-body-section-name-input'
ref='addStorageName'
value={this.state.newStorage.name}
onChange={(e) => this.handleAddStorageChange(e)}
/>
</div>
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Type</div>
<div styleName='addStorage-body-section-type'>
<select styleName='addStorage-body-section-type-select'
value={this.state.newStorage.type}
readOnly
>
<option value='FILESYSTEM'>File System</option>
</select>
<div styleName='addStorage-body-section-type-description'>
3rd party cloud integration(such as Google Drive and Dropbox) will be available soon.
</div>
</div>
</div>
<div styleName='addStorage-body-section'>
<div styleName='addStorage-body-section-label'>Location
</div>
<div styleName='addStorage-body-section-path'>
<input styleName='addStorage-body-section-path-input'
ref='addStoragePath'
placeholder='Select Folder'
value={this.state.newStorage.path}
onChange={(e) => this.handleAddStorageChange(e)}
/>
<button styleName='addStorage-body-section-path-button'
onClick={(e) => this.handleAddStorageBrowseButtonClick(e)}
>
...
</button>
</div>
</div>
<div styleName='addStorage-body-control'>
<button styleName='addStorage-body-control-createButton'
onClick={(e) => this.handleAddStorageCreateButton(e)}
>Create</button>
<button styleName='addStorage-body-control-cancelButton'
onClick={(e) => this.handleAddStorageCancelButton(e)}
>Cancel</button>
</div>
</div>
</div>
)
}
renderContent () {
switch (this.state.page) {
case 'ADD_STORAGE':
case 'ADD_FOLDER':
return this.renderAddStorage()
case 'LIST':
default:
return this.renderList()
}
}
render () {
return (
<div styleName='root'>
{this.renderContent()}
</div>
)
}
}
StoragesTab.propTypes = {
dispatch: PropTypes.func
}
export default CSSModules(StoragesTab, styles)

View File

@@ -0,0 +1,115 @@
.root
padding 15px
color $ui-text-color
.list
margin-bottom 15px
font-size 14px
.folderList
padding 0 15px
.folderList-item
height 30px
line-height 30px
border-bottom $ui-border
.folderList-empty
height 30px
line-height 30px
font-size 12px
color $ui-inactive-text-color
.list-empty
height 30px
color $ui-inactive-text-color
.list-control
height 30px
.list-control-addStorageButton
height 30px
padding 0 15px
border $ui-border
colorDefaultButton()
border-radius 2px
.addStorage
margin-bottom 15px
.addStorage-header
font-size 24px
color $ui-text-color
padding 5px
border-bottom $default-border
margin-bottom 15px
.addStorage-body-section
margin-bottom 15px
display flex
line-height 30px
.addStorage-body-section-label
width 150px
text-align right
margin-right 10px
.addStorage-body-section-name
flex 1
.addStorage-body-section-name-input
height 30px
vertical-align middle
width 150px
font-size 12px
border solid 1px $border-color
border-radius 2px
padding 0 5px
.addStorage-body-section-type
flex 1
.addStorage-body-section-type-select
height 30px
.addStorage-body-section-type-description
margin 5px
font-size 12px
color $ui-inactive-text-color
line-height 16px
.addStorage-body-section-path
flex 1
.addStorage-body-section-path-input
height 30px
vertical-align middle
width 150px
font-size 12px
border-style solid
border-width 1px 0 1px 1px
border-color $border-color
border-top-left-radius 2px
border-bottom-left-radius 2px
padding 0 5px
.addStorage-body-section-path-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
.addStorage-body-control
border-top $default-border
padding-top 10px
box-sizing border-box
height 40px
text-align right
.addStorage-body-control-createButton
colorPrimaryButton()
border none
border-radius 2px
height 30px
padding 0 15px
margin-right 5px
.addStorage-body-control-cancelButton
colorDefaultButton()
border $default-border
border-radius 2px
height 30px
padding 0 15px

View File

@@ -0,0 +1,112 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import ConfigTab from './ConfigTab'
import InfoTab from './InfoTab'
import StoragesTab from './StoragesTab'
import CSSModules from 'browser/lib/CSSModules'
import styles from './PreferencesModal.styl'
class Preferences extends React.Component {
constructor (props) {
super(props)
this.state = {
currentTab: 'STORAGES'
}
}
componentDidMount () {
this.refs.root.focus()
}
switchTeam (teamId) {
this.setState({currentTeamId: teamId})
}
handleNavButtonClick (tab) {
return (e) => {
this.setState({currentTab: tab})
}
}
renderContent () {
let { dispatch, config, storages } = this.props
switch (this.state.currentTab) {
case 'INFO':
return <InfoTab/>
case 'CONFIG':
return (
<ConfigTab
dispatch={dispatch}
config={config}
/>
)
case 'STORAGES':
default:
return (
<StoragesTab
dispatch={dispatch}
storages={storages}
/>
)
}
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
render () {
let content = this.renderContent()
let tabs = [
{target: 'STORAGES', label: 'Storages', icon: 'database'},
{target: 'CONFIG', label: 'Config', icon: 'cogs'},
{target: 'INFO', label: 'Info', icon: 'info-circle'}
]
let navButtons = tabs.map((tab) => {
let isActive = this.state.currentTab === tab.target
return (
<button styleName={isActive
? 'nav-button--active'
: 'nav-button'
}
key={tab.target}
onClick={(e) => this.handleNavButtonClick(tab.target)(e)}
>
<i styleName='nav-button-icon'
className={'fa fa-' + tab.icon}
/>
<span styleName='nav-button-label'>
{tab.label}
</span>
</button>
)
})
return (
<div styleName='root'
ref='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='nav'>
{navButtons}
</div>
<div styleName='content'>
{content}
</div>
</div>
)
}
}
Preferences.propTypes = {
dispatch: PropTypes.func
}
export default connect((x) => x)(CSSModules(Preferences, styles))

127
browser/main/store.js Normal file
View File

@@ -0,0 +1,127 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import ConfigManager from 'browser/main/lib/ConfigManager'
function storages (state = [], action) {
console.info('REDUX >> ', action)
switch (action.type) {
case 'INIT_ALL':
return action.storages
case 'ADD_STORAGE':
{
let storages = state.slice()
storages.push(action.storage)
return storages
}
case 'ADD_FOLDER':
case 'REMOVE_FOLDER':
case 'UPDATE_STORAGE':
case 'RENAME_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.storage.key)
storages.push(action.storage)
return storages
}
case 'REMOVE_STORAGE':
{
let storages = state.slice()
storages = storages
.filter((storage) => storage.key !== action.key)
return storages
}
}
return state
}
function notes (state = [], action) {
switch (action.type) {
case 'INIT_ALL':
return action.notes
case 'ADD_STORAGE':
{
let notes = state.concat(action.notes)
return notes
}
case 'REMOVE_STORAGE':
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.key)
return notes
}
case 'REMOVE_FOLDER':
{
let notes = state.slice()
notes = notes
.filter((note) => note.storage !== action.storage.key || note.folder !== action.key)
return notes
}
case 'CREATE_NOTE':
{
let notes = state.slice()
notes.push(action.note)
return notes
}
case 'UPDATE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
notes.push(action.note)
return notes
}
case 'MOVE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
notes.push(action.newNote)
return notes
}
case 'REMOVE_NOTE':
{
let notes = state.slice()
notes = notes.filter((note) => note.key !== action.note.key || note.folder !== action.note.folder || note.storage !== action.note.storage)
return notes
}
}
return state
}
const defaultConfig = ConfigManager.get()
function config (state = defaultConfig, action) {
switch (action.type) {
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
let reducer = combineReducers({
storages,
notes,
config,
routing: routerReducer
})
let store = createStore(reducer)
export default store

View File

@@ -3,17 +3,22 @@
@import '../mixins/*' @import '../mixins/*'
global-reset() global-reset()
@import '../shared/*' @import '../shared/*'
@import '../theme/*'
iptBgColor = #E6E6E6 iptBgColor = #E6E6E6
iptFocusBorderColor = #369DCD iptFocusBorderColor = #369DCD
DEFAULT_FONTS = 'Lato', 'MS Gothic', 'Malgun Gothic', 'Sans-serif'
body body
font-family "Lato" font-family DEFAULT_FONTS
color textColor color textColor
font-size fontSize font-size fontSize
width 100% width 100%
height 100% height 100%
overflow hidden overflow hidden
button, input
font-family "Lato"
.Finder .Finder
absolute top bottom left right absolute top bottom left right
@@ -46,6 +51,7 @@ body
width 250px width 250px
overflow-y auto overflow-y auto
z-index 0 z-index 0
user-select none
&>ul>li &>ul>li
.articleItem .articleItem
padding 10px padding 10px
@@ -115,5 +121,9 @@ body
overflow-y auto overflow-y auto
.MarkdownPreview .MarkdownPreview
marked() marked()
&.empty
color lighten(inactiveTextColor, 10%)
user-select none
font-size 14px
.CodeEditor .CodeEditor
absolute top bottom left right absolute top bottom left right

130
browser/styles/index.styl Normal file
View File

@@ -0,0 +1,130 @@
$brand-color = #6AA5E9
$danger-color = #c9302c
$danger-lighten-color = lighten(#c9302c, 5%)
// Layouts
$statusBar-height = 24px
$sideNav-width = 200px
$sideNav--folded-width = 44px
$topBar-height = 50px
// UI default
$ui-text-color = #515151
$ui-inactive-text-color = #939395
$ui-borderColor = #D1D1D1
$ui-backgroundColor = #FAFAFA
$ui-border = solid 1px $ui-borderColor
$ui-active-color = #6AA5E9
// UI Button
$ui-button-color = #939395
$ui-button--hover-backgroundColor = rgba(126, 127, 129, 0.08)
$ui-button--active-color = white
$ui-button--active-backgroundColor = #6AA5E9
$ui-button--focus-borderColor = lighten(#369DCD, 25%)
// UI Tooltip
$ui-tooltip-text-color = white
$ui-tooltip-backgroundColor = alpha(#444, 70%)
$ui-tooltip-button-backgroundColor = #D1D1D1
$ui-tooltip-button--hover-backgroundColor = lighten(#D1D1D1, 30%)
// UI Input
$ui-input--focus-borderColor = #369DCD
$ui-input--disabled-backgroundColor = #DDD
/*
* # Border
*/
$border-color = #D0D0D0
$active-border-color = #369DCD
$focus-border-color = #369DCD
$default-border = solid 1px $border-color
$active-border = solid 1px $active-border-color
/**
* # Button styles
*/
// Default button
$default-button-background = white
$default-button-background--hover = #e6e6e6
$default-button-background--active = #d4d4d4
colorDefaultButton()
background-color $default-button-background
&:hover
background-color $default-button-background--hover
&:active
background-color $default-button-background--active
&:active:hover
background-color $default-button-background--active
// Primary button(Brand color)
$primary-button-background = $brand-color
$primary-button-background--hover = darken($brand-color, 5%)
$primary-button-background--active = darken($brand-color, 10%)
colorPrimaryButton()
color white
background-color $primary-button-background
&:hover
background-color $primary-button-background--hover
&:active
background-color $primary-button-background--active
&:active:hover
background-color $primary-button-background--activ
// Danger button(Brand color)
$danger-button-background = #c9302c
$danger-button-background--hover = darken(#c9302c, 5%)
$danger-button-background--active = darken(#c9302c, 10%)
colorDangerButton()
color white
background-color $danger-button-background
&:hover
background-color $danger-button-background--hover
&:active
background-color $danger-button-background--active
&:active:hover
background-color $danger-button-background--active
/**
* Nav
*/
navButtonColor()
border none
color $ui-button-color
background-color transparent
transition color background-color 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
&:active
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:active, &:active:hover
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
/**
* # Modal Stuff
* These will be moved lib/modal
*/
$modal-z-index = 1002
$modal-background = white
$modal-margin = 64px auto 64px
$modal-border-radius = 5px
modal()
position relative
z-index $modal-z-index
width 100%
background-color $modal-background
overflow hidden
border-radius $modal-border-radius
box-shadow 2px 2px 10px gray

View File

@@ -1,329 +0,0 @@
noTagsColor = #999
iptFocusBorderColor = #369DCD
.ArticleDetail
absolute right bottom
top 60px
left 450px
padding 10px
background-color #E6E6E6
border-top 1px solid borderColor
border-left 1px solid borderColor
*
-webkit-user-select all
.deleteConfirm
width 100%
height 70px
.right
float right
button
cursor pointer
height 33px
padding 0 10px
margin-left 5px
font-size 14px
color inactiveTextColor
background-color darken(white, 5%)
border solid 1px borderColor
border-radius 5px
&:hover
background-color white
&.primary
border none
background-color brandColor
color white
&:hover
color white
background-color lighten(brandColor, 10%)
.detailInfo
height 70px
width 100%
font-size 12px
position relative
.left
absolute top left bottom
right 120px
.folderName
display inline-block
max-width 100px
overflow ellipsis
height 10px
.right
absolute top right
.detailBody
absolute left right bottom
top 70px
overflow-x hidden
overflow-y auto
.detailPanel
absolute top
left 10px
right 10px
bottom 10px
background-color white
border-radius 5px
border solid 1px borderColor
&>.header
absolute top left right
height 60px
.MarkdownPreview
absolute left right bottom
top 60px
marked()
box-sizing border-box
padding 5px 15px
border-top solid 1px borderColor
overflow-y auto
.CodeEditor
absolute left right bottom
top 60px
border-top solid 1px borderColor
min-height 300px
border-bottom-left-radius 5px
border-bottom-right-radius 5px
&.edit
.detailInfo
.left
&>.tutorial
position fixed
z-index 35
font-style italic
.folder
border none
width 150px
height 27px
outline none
background-color darken(white, 5%)
&:hover
background-color white
.TagSelect
.tags
white-space nowrap
overflow-x auto
position relative
max-width 350px
margin-top 5px
noSelect()
z-index 30
background-color #E6E6E6
.tagItem
background-color brandColor
border-radius 2px
color white
margin 0 2px
padding 0
border 1px solid darken(brandColor, 10%)
button.tagRemoveBtn
color white
border-radius 2px
border none
background-color transparent
padding 4px 2px
border-right 1px solid #E6E6E6
font-size 8px
line-height 12px
transition 0.1s
&:hover
background-color lighten(brandColor, 10%)
.tagLabel
padding 4px 4px
font-size 12px
line-height 12px
input.tagInput
background-color transparent
outline none
margin 0 2px
border-radius 2px
border none
transition 0.1s
height 18px
.suggestTags
position fixed
width 150px
max-height 150px
background-color white
z-index 5
border 1px solid borderColor
border-radius 5px
button
width 100%
display block
padding 0 15px
height 33px
line-height 33px
background-color transparent
border none
text-align left
font-size 14px
&:hover
background-color darken(white, 10%)
.right
button
cursor pointer
height 33px
width 55px
margin-left 5px
font-size 14px
color inactiveTextColor
background-color darken(white, 5%)
border solid 1px borderColor
border-radius 5px
&.preview
width inherit
&:hover
background-color white
&.primary
border none
background-color brandColor
color white
&:hover
color white
background-color lighten(brandColor, 10%)
.detailBody
.detailPanel
&>.header
&>.tutorial
fixed right
z-index 35
font-style italic
.mode
position relative
z-index 30
absolute top bottom right
display block
height 33px
margin-top 12px
width 150px
margin-right 15px
border-radius 5px
border solid 1px borderColor
transition 0.1s
&.idle
background-color darken(white, 5%)
cursor pointer
&:hover
background-color white
.ModeIcon
float left
width 25px
line-height 33px
text-align center
.modeLabel
line-height 30px
&.edit
background-color white
input
width 150px
line-height 30px
padding 0 10px
border none
outline none
background-color transparent
font-size 14px
.modeOptions
position fixed
width 150px
z-index 10
margin-top 5px
border 1px solid borderColor
border-radius 5px
background-color white
max-height 250px
overflow-y auto
.option
height 33px
line-height 33px
cursor pointer
&.active, &:hover.active
background-color brandColor
color white
.ModeIcon
width 30px
text-align center
display inline-block
&:hover
background-color darken(white, 10%)
.title
absolute left top bottom
right 150px
padding 0 15px
input
width 100%
border none
background-color transparent
line-height 60px
font-size 24px
outline none
&.idle
.detailInfo
&>.tutorial
fixed top right
z-index 35
font-style italic
.left
right 99px
.info
padding 5px
overflow ellipsis
.tags
padding 10px 10px 5px
color articleItemColor
a
background-color brandColor
color white
border-radius 2px
padding 1.5px 5px
margin 2px
font-size 10px
opacity 0.8
cursor pointer
&:hover
opacity 1
span.noTags
color noTagsColor
.right
z-index 30
button
border-radius 16.5px
cursor pointer
height 33px
width 33px
border none
margin-right 5px
font-size 18px
color inactiveTextColor
background-color darken(white, 5%)
padding 0
.tooltip
tooltip()
&.editBtn .tooltip
margin-top 25px
margin-left -45px
&.deleteBtn .tooltip
margin-top 25px
margin-left -73px
&:hover
color textColor
.tooltip
opacity 1
.detailBody
.detailPanel
&>.header
.mode
display block
line-height 60px
width 45px
height 60px
font-size 18px
text-align center
.title
absolute top bottom
left 45px
right 15px
font-size 24px
line-height 60px
white-space nowrap
overflow-x auto
overflow-y hidden

View File

@@ -1,74 +0,0 @@
articleItemHoverBgColor = darken(white, 5%)
articleItemColor = #777
.ArticleList
absolute bottom
top 60px
left 200px
width 250px
border-top 1px solid borderColor
border-right 1px solid borderColor
overflow-y auto
noSelect()
&>div
.articleItem
border solid 2px transparent
position relative
height 88px
width 100%
cursor pointer
transition 0.1s
background-color white
padding 0 10px
font-size 12px
.top
clearfix()
line-height 20px
padding 5px 0
color articleItemColor
.folderName
overflow ellipsis
display inline-block
width 120px
.updatedAt
float right
line-height 20px
.middle
padding 3px 0 7px
font-size 16px
position relative
height 26px
.mode
position absolute
left 0
font-size 12px
line-height 16px
.title
position absolute
left 19px
right 0
overflow ellipsis
.bottom
padding 5px 0
overflow-x auto
white-space nowrap
.tags
color articleItemColor
a
background-color brandColor
color white
border-radius 2px
padding 1.5px 5px
margin 2px
font-size 10px
opacity 0.8
&:hover
opacity 1
&:hover, &.hover
background-color articleItemHoverBgColor
&:active, &.active
background-color white
&:active, &.active
border-color brandBorderColor
.divider
border-bottom solid 1px borderColor

View File

@@ -1,170 +0,0 @@
articleNavBgColor = #353535
articleCount = #999
.ArticleNavigator
background-color articleNavBgColor
absolute top bottom left
width 200px
border-right 1px solid borderColor
color white
.userInfo
height 60px
display block
border-bottom 1px solid borderColor
.userProfileName
color brandColor
font-size 28px
padding 6px 0 0 10px
white-space nowrap
text-overflow ellipsis
overflow hidden
.userName
color white
padding-left 20px
margin-top 3px
.tutorial
position fixed
z-index 35
top 0
left 0
pointer-event none
font-style italic
transition 0.1s
&.hide
opacity 0
.settingBtn
width 22px
height 22px
line-height 22px
border-radius 11px
position absolute
top 19px
right 14px
color white
padding 0
background-color transparent
border 1px solid white
z-index 31
.tooltip
tooltip()
margin-top -5px
margin-left 10px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.controlSection
height 88px
padding 22px 15px
margin-bottom 44px
.tutorial
fixed top left
z-index 35
pointer-event none
font-style italic
transition 0.1s
&.hide
opacity 0
.newPostBtn
position relative
border none
background-color brandColor
color white
height 44px
width 170px
border-radius 5px
font-size 20px
transition 0.1s
z-index 30
.tooltip
tooltip()
margin-left 48px
margin-top -3px
&:hover
background-color lighten(brandColor, 7%)
.tooltip
opacity 1
.folders, .members
.header
border-bottom 1px solid borderColor
padding-bottom 5px
margin-bottom 10px
clearfix()
position relative
z-index 30
.title
float left
padding-left 10px
font-size 18px
line-height 22px
.addBtn
float right
margin-right 15px
width 22px
height 22px
font-size 10px
padding 0
line-height 22px
border 1px solid white
border-radius 11px
background-color transparent
color white
padding 0
font-weight bold
.tooltip
tooltip()
margin-top -6px
margin-left 11px
&:hover
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor
.folders
absolute bottom
top 200px
width 100%
.header
.tutorial
position fixed
z-index 35px
top 200px
font-style italic
.folderList
absolute bottom
top 38px
overflow-y auto
.folderList button
height 33px
width 199px
border none
text-align left
font-size 14px
background-color transparent
color white
padding-left 15px
overflow ellipsis
&:hover
background-color transparentify(white, 5%)
&.active, &:active
background-color transparentify(lighten(brandColor, 25%), 70%)
.articleCount
color articleCount
font-size 12px
.members
.memberList>div
height 33px
width 200px
margin-bottom 5px
padding-left 15px
.memberImage
float left
margin-top 5.5px
border-radius 11px
.memberProfileName
float left
line-height 33px
margin-left 7px

View File

@@ -1,156 +0,0 @@
bgColor = #E6E6E6
inputBgColor = white
iptFocusBorderColor = #369DCD
topBarBtnColor = #B3B3B3
topBarBtnBgColor = #B3B3B3
topBarBtnBgActiveColor = #3A3A3A
infoBtnColor = bgColor
infoBtnBgColor = #B3B3B3
infoBtnActiveBgColor = #3A3A3A
.ArticleTopBar
absolute top right
left 200px
height 60px
background-color bgColor
&>.tutorial
.clickJammer
fixed top left bottom right
z-index 40
background transparent
.global
fixed bottom right
height 100px
z-index 35
font-style italic
.finder
fixed bottom right
height 250px
left 50%
margin-left -250px
z-index 35
font-style italic
.back
fixed top left bottom right
z-index 20
background-color transparentify(black, 80%)
&>.left
float left
&>.tutorial
fixed top
left 200px
z-index 36
font-style italic
&>.search
position relative
float left
height 33px
margin-top 13.5px
margin-left 15px
width 350px
padding 5px 15px
transition 0.1s
font-size 16px
border 1px solid transparent
z-index 30
.tooltip
tooltip()
margin-left -24px
margin-top 35px
opacity 1
&.hide
opacity 0
input
absolute top left
width 350px
border-radius 16.5px
background-color inputBgColor
border 1px solid transparent
padding-left 35px
outline none
font-size 14px
height 33px
line-height 33px
z-index 0
&:focus
border-color iptFocusBorderColor
i.fa.fa-search
position absolute
display block
top 0
left 10px
line-height 33px
z-index 1
pointer-events none
.searchClearBtn
position absolute
top 6px
right 10px
width 20px
height 20px
border-radius 10px
border none
background-color transparent
color topBarBtnColor
transition 0.1s
line-height 20px
text-align center
padding 0
&:hover
color white
background-color topBarBtnBgColor
&>.refreshBtn
float left
width 33px
height 33px
margin-top 13.5px
margin-left 15px
border none
color refreshBtColor
background transparent
font-size 18px
line-height 18px
transition 0.1s
&:hover
color refreshBtnActiveColor
&>.right
float right
&>button
display block
position absolute
right 74px
top 20px
width 20px
height 20px
font-size 14px
line-height 14px
background-color infoBtnBgColor
color bgColor
border-radius 11px
border none
transition 0.1s
.tooltip
tooltip()
margin-left -50px
margin-top 29px
&:hover
background-color infoBtnActiveBgColor
.tooltip
opacity 1
&>.logo
display block
position absolute
top 8px
right 15px
opacity 0.7
.tooltip
tooltip()
margin-top 44px
margin-left -120px
&:hover
opacity 1
.tooltip
opacity 1

View File

@@ -1,86 +0,0 @@
userNavigatorBgColor = #1B1C1C
userNavigatorColor = #DDD
userAnchorColor = #979797
userAnchorBgColor = #BEBEBE
userAnchorActiveColor = textColor
userAnchorActiveBgColor = white
.UserNavigator
noSelect()
background-color userNavigatorBgColor
absolute left top bottom
width 60px
text-align center
box-sizing border-box
ul.userList
position absolute
top 25px
left 0
right 0
bottom 70px
// overflow-y auto
&>li
a
display block
width 38px
height 64px
margin 0 auto 10px
text-align center
text-decoration none
color userAnchorColor
line-height 44px
font-size 1.1em
cursor pointer
transition 0.1s
img.ProfileImage
width 38px
height 38px
border-radius 22px
opacity 0.7
&:hover
img.ProfileImage
opacity 1
.userTooltip
opacity 1
&.active
img.ProfileImage
opacity 1
.userTooltip
tooltip()
position absolute
margin-top -52px
margin-left 44px
.keyLabel
margin-top -25px
font-size 0.8em
color userNavigatorColor
button.createTeamBtn
display block
margin 0 auto
width 30px
height 30px
border-radius 15px
border 2px solid darken(white, 5%)
color darken(white, 5%)
text-align center
background-image none
background-color transparent
box-sizing border-box
absolute left right
bottom 15px
font-size 22px
line-height 22px
transition 0.1s
.tooltip
tooltip()
margin-top -26px
margin-left 30px
&:hover, &.hover, &:focus, &.focus
color white
border-color white
.tooltip
opacity 1
&:active
background-color brandColor
border-color brandColor

Some files were not shown because too many files have changed in this diff Show More