1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 02:06:29 +00:00

Compare commits

...

255 Commits

Author SHA1 Message Date
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
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
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
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
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
219 changed files with 5660 additions and 5299 deletions

View File

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

6
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.env
node_modules/*
!node_modules/boost
Boost-darwin-x64/
backup/
compiled
/dist
/compiled
/secret

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,264 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
const ace = window.ace
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.changeHandler = e => this.handleChange(e)
this.blurHandler = (e) => {
if (e.relatedTarget === null) {
return
}
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
})
if (isFocusingToSearch) {
return
}
if (this.props.onBlur) this.props.onBlur(e)
}
this.killedBuffer = ''
this.execHandler = (e) => {
console.log(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 = {
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size']
}
this.silentChange = false
}
componentWillReceiveProps (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
}
}
componentDidMount () {
let { article } = this.props
var el = ReactDOM.findDOMNode(this)
var editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/xcode')
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(this.state.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 cursor up',
bindKey: {mac: 'Ctrl-Y'},
exec: function (editor) {
editor.insert(this.killedBuffer)
}.bind(this),
readOnly: true
})
editor.commands.addCommand({
name: 'Focus title',
bindKey: {win: 'Esc', mac: 'Esc'},
exec: function (editor, e) {
let currentWindow = remote.getCurrentWebContents()
if (config['switch-preview'] === 'rightclick') {
currentWindow.send('detail-preview')
}
currentWindow.send('list-focus')
},
readOnly: true
})
editor.commands.on('exec', this.execHandler)
editor.commands.on('afterExec', this.afterExecHandler)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: article.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.setValue(this.props.article.content)
session.on('change', this.changeHandler)
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
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) {
var session = this.editor.getSession()
if (this.props.article.key !== prevProps.article.key) {
session.removeListener('change', this.changeHandler)
session.setValue(this.props.article.content)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
}
if (prevProps.article.mode !== this.props.article.mode) {
let mode = _.findWhere(modes, {name: this.props.article.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size']
}, function () {
var session = this.editor.getSession()
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
})
}
handleChange (e) {
if (this.props.onChange) {
var value = this.editor.getValue()
this.props.onChange(value)
}
}
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)
}
render () {
return (
<div
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + ', monospace'
}}
/>
)
}
}
CodeEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
mode: PropTypes.string,
key: PropTypes.string
}),
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
readOnly: PropTypes.bool
}
CodeEditor.defaultProps = {
readOnly: false
}
export default CodeEditor

View File

@@ -1,5 +1,6 @@
import React, { PropTypes } from 'react'
import shell from 'shell'
const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component {
handleClick (e) {

View File

@@ -0,0 +1,222 @@
import React, { PropTypes } from 'react'
import markdown from '../lib/markdown'
import ReactDOM from 'react-dom'
import sanitizeHtml from '@rokt33r/sanitize-html'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
const electron = require('electron')
const shell = electron.shell
const ipc = electron.ipcRenderer
const katex = window.katex
const OSX = global.process.platform === 'darwin'
const sanitizeOpts = {
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
allowedClasses: {
'a': ['lineAnchor'],
'div': ['math'],
'span': ['math', 'hljs-*'],
'code': ['language-*']
},
allowedAttributes: {
a: ['href', 'data-key'],
img: [ 'src' ],
label: ['for'],
input: ['checked', 'type'],
'*': ['id', 'name']
},
transformTags: {
'*': function (tagName, attribs) {
let href = attribs.href
if (tagName === 'input' && attribs.type !== 'checkbox') {
return false
}
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
return {
tagName: tagName,
attribs: attribs
}
}
}
}
function handleAnchorClick (e) {
if (this.attributes.href && this.attributes.href.nodeValue.match(/^#.+/)) {
return
}
e.preventDefault()
e.stopPropagation()
let href = this.href
if (href && href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
shell.openExternal(href)
}
}
function stopPropagation (e) {
e.preventDefault()
e.stopPropagation()
}
function math2Katex (display) {
return function (el) {
try {
katex.render(el.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&'), el, {display: display})
el.className = 'math-rendered'
} catch (e) {
el.innerHTML = e.message
el.className = 'math-failed'
}
}
}
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family']
}
}
componentDidMount () {
this.addListener()
this.renderMath()
ipc.on('config-apply', this.configApplyHandler)
}
componentDidUpdate () {
this.addListener()
this.renderMath()
}
componentWillUnmount () {
this.removeListener()
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillUpdate () {
this.removeListener()
}
renderMath () {
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
Array.prototype.forEach.call(inline, math2Katex(false))
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
Array.prototype.forEach.call(block, math2Katex(true))
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener('click', handleAnchorClick)
anchor.addEventListener('mousedown', stopPropagation)
anchor.addEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.addEventListener('click', stopPropagation)
})
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.removeEventListener('click', handleAnchorClick)
anchor.removeEventListener('mousedown', stopPropagation)
anchor.removeEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.removeEventListener('click', stopPropagation)
})
}
handleClick (e) {
if (this.props.onClick) {
this.props.onClick(e)
}
}
handleDoubleClick (e) {
if (this.props.onDoubleClick) {
this.props.onDoubleClick(e)
}
}
handleMouseDown (e) {
if (this.props.onMouseDown) {
this.props.onMouseDown(e)
}
}
handleMouseUp (e) {
if (this.props.onMouseUp) {
this.props.onMouseUp(e)
}
}
handleMouseMove (e) {
if (this.props.onMouseMove) {
this.props.onMouseMove(e)
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family']
})
}
render () {
let isEmpty = this.props.content.trim().length === 0
let content = isEmpty
? '(Empty content)'
: this.props.content
content = markdown(content)
content = sanitizeHtml(content, sanitizeOpts)
return (
<div
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
onClick={e => this.handleClick(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
onMouseDown={e => this.handleMouseDown(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}
dangerouslySetInnerHTML={{__html: ' ' + content}}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
}}
/>
)
}
}
MarkdownPreview.propTypes = {
onClick: PropTypes.func,
onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
content: PropTypes.string
}

View File

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

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from 'boost/linkState'
import linkState from '../lib/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
@@ -117,10 +117,10 @@ export default class TagSelect extends React.Component {
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<span key={tag} className='tagItem'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='tagRemoveBtn'><i className='fa fa-fw fa-times'/></button>
<span className='tagLabel'>{tag}</span>
</span>))
<div key={tag} className='TagSelect-tags-item'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='TagSelect-tags-item-remove'><i className='fa fa-fw fa-times'/></button>
<div className='TagSelect-tags-item-label'>{tag}</div>
</div>))
: null
let suggestElements = this.shouldShowSuggest() ? suggestTags
@@ -134,7 +134,7 @@ export default class TagSelect extends React.Component {
return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='tags'>
<div className='TagSelect-tags'>
{tagElements}
<input
type='text'
@@ -142,13 +142,13 @@ export default class TagSelect extends React.Component {
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='tagInput'
className='TagSelect-input'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='suggestTags'>
<div ref='suggestTags' className='TagSelect-suggest'>
{suggestElements}
</div>
)

View File

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

View File

@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import ModeIcon from 'browser/components/ModeIcon'
import { selectArticle } from './actions'
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')
let data = JSON.parse(localStorage.getItem('local'))
if (data == null) return null
let { folders, articles } = data
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,18 +6,31 @@ import { createStore } from 'redux'
import FinderInput from './FinderInput'
import FinderList from './FinderList'
import FinderDetail from './FinderDetail'
import { selectArticle, searchArticle, refreshData } from './actions'
import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash'
import activityRecord from 'boost/activityRecord'
import dataStore from 'browser/lib/dataStore'
import remote from 'remote'
var hideFinder = remote.getGlobal('hideFinder')
import clipboard from 'clipboard'
const electron = require('electron')
const { clipboard, ipcRenderer, remote } = electron
const path = require('path')
var notifier = require('node-notifier')
var path = require('path')
function getIconPath () {
return path.resolve(global.__dirname, '../../resources/favicon-230x230.png')
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('../styles/finder/index.styl')
@@ -33,11 +46,21 @@ class FinderMain extends React.Component {
}
componentDidMount () {
this.keyDownHandler = e => this.handleKeyDown(e)
document.addEventListener('keydown', this.keyDownHandler)
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) {
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
componentWillUnmount () {
document.removeEventListener('keydown', this.keyDownHandler)
window.removeEventListener('focus', this.focusHandler)
}
handleKeyDown (e) {
@@ -59,17 +82,21 @@ class FinderMain extends React.Component {
hideFinder()
e.preventDefault()
}
if (e.keyCode === 91 || e.metaKey) {
return
}
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
}
saveToClipboard () {
let { activeArticle } = this.props
clipboard.writeText(activeArticle.content)
activityRecord.emit('FINDER_COPY')
notifier.notify({
icon: getIconPath(),
'title': 'Saved to Clipboard!',
'message': 'Paste it wherever you want!'
ipcRenderer.send('copy-finder')
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
hideFinder()
}
@@ -102,7 +129,7 @@ class FinderMain extends React.Component {
let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard()
return (
<div onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'>
<div className='Finder'>
<FinderInput
handleSearchChange={e => this.handleSearchChange(e)}
ref='finderInput'
@@ -156,6 +183,14 @@ function buildFilter (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) {
let { articles, folders, status } = state
@@ -172,10 +207,10 @@ function remap (state) {
let targetFolders
if (folders != null) {
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 => {
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)
@@ -188,7 +223,7 @@ function remap (state) {
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'))
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
@@ -196,7 +231,7 @@ function remap (state) {
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')))
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
@@ -205,7 +240,6 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
console.log(status.search)
return {
articles,
activeArticle,
@@ -216,13 +250,19 @@ function remap (state) {
var Finder = connect(remap)(FinderMain)
var store = createStore(reducer)
function refreshData () {
let data = dataStore.getData(true)
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => {
store.dispatch(refreshData())
activityRecord.emit('FINDER_OPEN')
refreshData()
}
ReactDOM.render((
<Provider store={store}>
<Finder/>
</Provider>
), document.getElementById('content'))
), document.getElementById('content'), function () {
refreshData()
})

View File

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

View File

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

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
}

View File

@@ -0,0 +1,21 @@
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const userDataPath = remote.app.getPath('userData')
const configFile = 'config.json'
const defaultConfig = {
'editor-font-size': '14',
'editor-font-family': 'Monaco, Consolas',
'editor-indent-type': 'space',
'editor-indent-size': '4',
'preview-font-size': '14',
'preview-font-family': 'Lato',
'switch-preview': 'blur',
'disable-direct-write': false
}
export default function fetchConfig () {
return Object.assign({}, defaultConfig, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
}

View File

@@ -2,6 +2,6 @@ var crypto = require('crypto')
module.exports = function () {
var shasum = crypto.createHash('sha1')
shasum.update(((new Date()).getTime()).toString())
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
return shasum.digest('hex')
}

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

@@ -0,0 +1,48 @@
import markdownit from 'markdown-it'
import emoji from 'markdown-it-emoji'
import math from 'markdown-it-math'
import hljs from 'highlight.js'
var md = markdownit({
typographer: true,
linkify: true,
html: true,
xhtmlOut: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value
} catch (e) {}
}
return str.replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;')
}
})
md.use(emoji, {
shortcuts: {}
})
md.use(math, {
inlineRenderer: function (str) {
return `<span class='math'>${str}</span>`
},
blockRenderer: function (str) {
return `<div class='math'>${str}</div>`
}
})
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,6 +1,8 @@
import React from 'react'
import ReactDOM from 'react-dom'
const remote = require('electron').remote
class ModalBase extends React.Component {
constructor (props) {
super(props)
@@ -13,6 +15,8 @@ class ModalBase extends React.Component {
close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
remote.getCurrentWebContents().send('list-focus')
}
render () {
@@ -38,9 +42,15 @@ export function openModal (component, props) {
export function closeModal () {
if (modalBase == null) { return }
modalBase.setState({component: null, componentProps: null, isHidden: true})
modalBase.close()
}
export function isModalOpen () {
return !modalBase.state.isHidden
}
export default {
open: openModal,
close: closeModal,
isOpen: isModalOpen
}

View File

@@ -68,7 +68,7 @@ const modes = [
{
name: 'csharp',
label: 'C#',
alias: ['cs'],
alias: ['cs', 'c#'],
mode: 'csharp'
},
{

View File

@@ -1,4 +1,5 @@
var shell = require('shell')
const electron = require('electron')
const shell = electron.shell
export default function (e) {
shell.openExternal(e.currentTarget.href)

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

@@ -0,0 +1,225 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
import activityRecord from 'browser/lib/activityRecord'
import fetchConfig from 'browser/lib/fetchConfig'
const electron = require('electron')
const ipc = electron.ipcRenderer
export const PREVIEW_MODE = 'PREVIEW_MODE'
export const EDIT_MODE = 'EDIT_MODE'
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
export default class ArticleEditor extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.isMouseDown = false
this.state = {
status: PREVIEW_MODE,
cursorPosition: null,
firstVisibleRow: null,
switchPreview: config['switch-preview'],
isTemporary: false
}
}
componentDidMount () {
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillReceiveProps (nextProps) {
if (nextProps.article.key !== this.props.article.key) {
this.setState({
content: this.props.article.content
})
}
}
handleConfigApply (e, newConfig) {
this.setState({
switchPreview: newConfig['switch-preview']
})
}
resetCursorPosition () {
this.setState({
cursorPosition: null,
firstVisibleRow: null
}, function () {
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
if (previewEl) previewEl.scrollTop = 0
})
}
switchPreviewMode (isTemporary = false) {
if (this.props.article.mode !== 'markdown') return true
let cursorPosition = this.refs.editor.getCursorPosition()
let firstVisibleRow = this.refs.editor.getFirstVisibleRow()
this.setState({
status: PREVIEW_MODE,
cursorPosition,
firstVisibleRow,
isTemporary: isTemporary
}, function () {
let previewEl = ReactDOM.findDOMNode(this.refs.preview)
let anchors = previewEl.querySelectorAll('.lineAnchor')
for (let i = 0; i < anchors.length; i++) {
if (parseInt(anchors[i].dataset.key, 10) > cursorPosition.row || i === anchors.length - 1) {
var targetAnchor = anchors[i > 0 ? i - 1 : 0]
previewEl.scrollTop = targetAnchor.offsetTop - 100
break
}
}
})
}
switchEditMode (isTemporary = false) {
this.setState({
status: EDIT_MODE,
isTemporary: false
}, function () {
if (this.state.cursorPosition != null) {
this.refs.editor.moveCursorTo(this.state.cursorPosition.row, this.state.cursorPosition.column)
this.refs.editor.scrollToLine(this.state.firstVisibleRow)
}
this.refs.editor.editor.focus()
if (!isTemporary) activityRecord.emit('ARTICLE_UPDATE', this.props.article)
})
}
handleBlurCodeEditor (e) {
let isFocusingToThis = e.relatedTarget === ReactDOM.findDOMNode(this)
if (isFocusingToThis || this.state.switchPreview !== 'blur') {
return
}
let { article } = this.props
if (article.mode === 'markdown') {
this.switchPreviewMode()
}
}
handleCodeEditorChange (value) {
this.props.onChange(value)
}
handleRightClick (e) {
let { article } = this.props
if (this.state.switchPreview === 'rightclick' && article.mode === 'markdown') {
if (this.state.status === EDIT_MODE) this.switchPreviewMode()
else this.switchEditMode()
}
}
handleMouseUp (e) {
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isMouseDown = false
this.moveCount = 0
if (!this.isDrag) {
this.switchEditMode()
}
break
case 2:
if (this.state.isTemporary) this.switchEditMode(true)
}
break
case 'rightclick':
}
}
handleMouseMove (e) {
if (this.state.switchPreview === 'blur' && this.isMouseDown) {
this.moveCount++
if (this.moveCount > 5) {
this.isDrag = true
}
}
}
handleMouseDowm (e) {
switch (this.state.switchPreview) {
case 'blur':
switch (e.button) {
case 0:
this.isDrag = false
this.isMouseDown = true
this.moveCount = 0
break
case 2:
if (this.state.status === EDIT_MODE && this.props.article.mode === 'markdown') {
this.switchPreviewMode(true)
}
}
break
case 'rightclick':
}
}
render () {
let { article } = this.props
let showPreview = article.mode === 'markdown' && this.state.status === PREVIEW_MODE
return (
<div
tabIndex='5'
onContextMenu={e => this.handleRightClick(e)}
onMouseUp={e => this.handleMouseUp(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseDown={e => this.handleMouseDowm(e)}
className='ArticleEditor'
>
{showPreview
? <MarkdownPreview
ref='preview'
content={article.content}
/>
: <CodeEditor
ref='editor'
onBlur={e => this.handleBlurCodeEditor(e)}
onChange={value => this.handleCodeEditorChange(value)}
article={article}
/>
}
{article.mode === 'markdown'
? <div className='ArticleDetail-panel-content-tooltip' children={
showPreview
? this.state.switchPreview === 'blur'
? 'Click to Edit'
: 'Right Click to Edit'
: this.state.switchPreview === 'blur'
? 'Press ESC to Watch Preview'
: 'Right Click to Watch Preview'
}
/>
: null
}
</div>
)
}
}
ArticleEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
key: PropTypes.string,
mode: PropTypes.string
}),
onChange: PropTypes.func,
parent: PropTypes.object
}

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,361 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import {
switchFolder,
updateArticle
} from '../../actions'
import linkState from 'browser/lib/linkState'
import TagSelect from 'browser/components/TagSelect'
import ModeSelect from 'browser/components/ModeSelect'
import ShareButton from './ShareButton'
import { openModal, isModalOpen } from 'browser/lib/modal'
import DeleteArticleModal from '../../modal/DeleteArticleModal'
import ArticleEditor from './ArticleEditor'
const electron = require('electron')
const ipc = electron.ipcRenderer
import fetchConfig from 'browser/lib/fetchConfig'
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
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>
)
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
this.deleteHandler = e => {
if (isModalOpen()) return true
this.handleDeleteButtonClick()
}
this.uncacheHandler = e => {
if (isModalOpen()) return true
this.handleUncache()
}
this.titleHandler = e => {
if (isModalOpen()) return true
if (this.refs.title) {
this.focusTitle()
}
}
this.editHandler = e => {
if (isModalOpen()) return true
if (this.refs.editor) this.refs.editor.switchEditMode()
}
this.previewHandler = e => {
if (isModalOpen()) return true
if (this.refs.editor) this.refs.editor.switchPreviewMode()
}
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
article: Object.assign({content: ''}, props.activeArticle),
openShareDropdown: false,
fontFamily: config['editor-font-family']
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
this.shareDropdownInterceptor = e => {
e.stopPropagation()
}
ipc.on('detail-delete', this.deleteHandler)
ipc.on('detail-uncache', this.uncacheHandler)
ipc.on('detail-title', this.titleHandler)
ipc.on('detail-edit', this.editHandler)
ipc.on('detail-preview', this.previewHandler)
ipc.on('config-apply', this.configApplyHandler)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ipc.removeListener('detail-delete', this.deleteHandler)
ipc.removeListener('detail-uncache', this.uncacheHandler)
ipc.removeListener('detail-title', this.titleHandler)
ipc.removeListener('detail-edit', this.editHandler)
ipc.removeListener('detail-preview', this.previewHandler)
}
componentDidUpdate (prevProps, prevState) {
if (this.props.activeArticle == null || prevProps.activeArticle == null || this.props.activeArticle.key !== prevProps.activeArticle.key) {
if (this.refs.editor) this.refs.editor.resetCursorPosition()
}
if (prevProps.activeArticle == null && this.props.activeArticle) {
}
}
handleConfigApply (e, config) {
this.setState({
fontFamily: config['editor-font-family']
})
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
<div className='ArticleDetail-empty-box'>
<div className='ArticleDetail-empty-box-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
handleOthersButtonClick (e) {
this.deleteHandler()
}
handleFolderKeyChange (e) {
let { dispatch, activeArticle, status, folders } = this.props
let article = Object.assign({}, activeArticle, {
FolderKey: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
let targetFolderKey = e.target.value
if (status.targetFolders.length > 0) {
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
dispatch(switchFolder(targetFolder.name))
}
}
handleTitleChange (e) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
title: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleTagsChange (newTag, tags) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
tags: tags,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleModeChange (value) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
mode: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
this.switchEditMode()
}
handleContentChange (value) {
let { dispatch, activeArticle } = this.props
if (activeArticle.content !== value) {
let article = Object.assign({}, activeArticle, {
content: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
}
handleDeleteButtonClick (e) {
if (this.props.activeArticle) {
openModal(DeleteArticleModal, {articleKey: this.props.activeArticle.key})
}
}
handleTitleKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.refs.mode.handleIdleSelectClick()
}
}
handleModeSelectKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.switchEditMode()
}
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault()
this.focusTitle()
}
if (e.keyCode === 27) {
this.focusTitle()
}
}
switchEditMode () {
this.refs.editor.switchEditMode()
}
focusTitle () {
if (this.refs.title) {
let titleEl = ReactDOM.findDOMNode(this.refs.title)
titleEl.focus()
titleEl.select()
}
}
render () {
let { folders, status, tags, activeArticle, modified, user } = this.props
if (activeArticle == null) return this.renderEmpty()
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
return (
<div tabIndex='4' className='ArticleDetail'>
<div className='ArticleDetail-info'>
<div className='ArticleDetail-info-row'>
<select
className='ArticleDetail-info-folder'
value={activeArticle.FolderKey}
onChange={e => this.handleFolderKeyChange(e)}
>
{folderOptions}
</select>
<span className='ArticleDetail-info-status'
children={
isUnsaved
? <span> <span className='unsaved-mark'></span> Unsaved</span>
: `Created : ${moment(activeArticle.createdAt).format('YYYY/MM/DD')} Updated : ${moment(activeArticle.updatedAt).format('YYYY/MM/DD')}`
}
/>
<div className='ArticleDetail-info-control'>
{/*<div className={'ArticleDetail-info-control-save' + (!isUnsaved ? ' hide' : '')}>
<button
onClick={e => this.handleSaveButtonClick(e)}
className='ArticleDetail-info-control-save-button'
disabled={!isUnsaved}
>
<i className='fa fa-fw fa-save'/>&nbsp;Save
<span className='tooltip' children={`Save Post (${OSX ? '⌘' : '^'} + S)`}/>
</button>
</div>*/}
<ShareButton
article={activeArticle}
user={user}
/>
<button className='ArticleDetail-info-control-delete-button' onClick={e => this.handleOthersButtonClick(e)}>
<i className='fa fa-fw fa-trash'/>
<span className='tooltip' children={`Delete Post (^ + Del)`}/>
</button>
</div>
</div>
<div className='ArticleDetail-info-row2'>
<TagSelect
tags={activeArticle.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/>
{status.isTutorialOpen ? tagSelectTutorialElement : null}
</div>
</div>
<div className='ArticleDetail-panel'>
<div className='ArticleDetail-panel-header'>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder='(Untitled)'
ref='title'
value={activeArticle.title}
onChange={e => this.handleTitleChange(e)}
style={{
fontFamily: this.state.fontFamily
}}
/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
onKeyDown={e => this.handleModeSelectKeyDown(e)}
value={activeArticle.mode}
className='ArticleDetail-panel-header-mode'
/>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
</div>
<ArticleEditor
ref='editor'
article={activeArticle}
onChange={content => this.handleContentChange(content)}
/>
</div>
</div>
)
}
}
ArticleDetail.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape(),
tags: PropTypes.array,
user: PropTypes.shape(),
folders: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}
ArticleDetail.prototype.linkState = linkState

View File

@@ -1,19 +1,32 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon'
import ModeIcon from 'browser/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 { switchArticle } from '../actions'
import FolderMark from 'browser/components/FolderMark'
import TagLink from './TagLink'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
export default class ArticleList extends React.Component {
constructor (props) {
super(props)
this.focusHandler = e => this.focus()
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ipc.on('list-focus', this.focusHandler)
this.focus()
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ipc.removeListener('list-focus', this.focusHandler)
}
componentDidUpdate () {
@@ -36,6 +49,10 @@ export default class ArticleList extends React.Component {
}
}
focus () {
ReactDOM.findDOMNode(this).focus()
}
// 移動ができなかったらfalseを返す:
selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props
@@ -64,36 +81,107 @@ export default class ArticleList extends React.Component {
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key))
}
}
handleArticleListKeyDown (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()
remote.getCurrentWebContents().send('detail-edit')
}
if (e.keyCode === 83) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-save')
}
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorArticle()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextArticle()
}
}
render () {
let { articles, activeArticle, folders } = this.props
let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let modifiedArticle = _.findWhere(modified, {key: article.key})
let originalArticle = article
if (modifiedArticle) {
article = Object.assign({}, article)
}
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.map(tag => {
? article.tags.slice().map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
let folderChanged = originalArticle.FolderKey !== article.FolderKey
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
let title = article.title.trim().length === 0
? <small>(Untitled)</small>
: article.title
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'>
<div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='ArticleList-item-top'>
{folder != null
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span>
? folderChanged
? <span className='folderName'>
<FolderMark color={originalFolder.color}/>{originalFolder.name}
->
<FolderMark color={folder.color}/>{folder.name}
</span>
: <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>
<span className='updatedAt'
children={
modifiedArticle != null
? <span><span className='unsaved-mark'></span> Unsaved</span>
: moment(article.updatedAt).fromNow()
}
/>
</div>
<div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== NEW ? article.title : '(New article)'}</div>
<div className='ArticleList-item-middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div>
<div className='bottom'>
<div className='ArticleList-item-middle2'>
<pre><code children={article.content.trim().length === 0 ? '(Empty content)' : article.content.substring(0, 50)}/></pre>
</div>
<div className='ArticleList-item-bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div>
</div>
@@ -103,7 +191,7 @@ export default class ArticleList extends React.Component {
})
return (
<div className='ArticleList'>
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
{articleElements}
</div>
)
@@ -111,8 +199,9 @@ export default class ArticleList extends React.Component {
}
ArticleList.propTypes = {
dispatch: PropTypes.func,
folders: PropTypes.array,
articles: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}

View File

@@ -1,15 +1,17 @@
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 { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
import { openModal, isModalOpen } from 'browser/lib/modal'
import FolderMark from 'browser/components/FolderMark'
import Preferences from '../modal/Preferences'
import CreateNewFolder from '../modal/CreateNewFolder'
import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
import remote from 'remote'
let userName = remote.getGlobal('process').env.USER
const ipc = require('electron').ipcRenderer
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const preferenceTutorialElement = (
<svg width='300' height='300' className='tutorial'>
@@ -26,53 +28,44 @@ c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
</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'/>
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
<svg x='50' y='10' width='300' height='400'>
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
</svg>
</svg>
)
export default class ArticleNavigator extends React.Component {
constructor (props) {
super(props)
this.newFolderHandler = e => {
if (isModalOpen()) return true
this.handleNewFolderButton(e)
}
}
componentDidMount () {
ipc.on('nav-new-folder', this.newFolderHandler)
}
componentWillUnmount () {
ipc.removeListener('nav-new-folder', this.newFolderHandler)
}
handlePreferencesButtonClick (e) {
openModal(Preferences)
}
handleNewPostButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(CREATE_MODE))
}
handleNewFolderButton (e) {
let { activeUser } = this.props
openModal(CreateNewFolder, {user: activeUser})
let { user } = this.props
openModal(CreateNewFolder, {user: user})
}
handleFolderButtonClick (name) {
@@ -87,14 +80,58 @@ export default class ArticleNavigator extends React.Component {
dispatch(setSearchFilter(''))
}
handleUnsavedItemClick (article) {
let { dispatch } = this.props
return e => {
let { articles } = this.props
let isInArticleList = articles.some(_article => _article.key === article.key)
if (!isInArticleList) dispatch(clearSearch())
dispatch(switchArticle(article.key))
}
}
handleUncacheButtonClick (article) {
let { dispatch } = this.props
return e => {
dispatch(uncacheArticle(article.key))
}
}
handleSaveAllClick (e) {
let { dispatch } = this.props
dispatch(saveAllArticles())
}
render () {
let { status, folders, allArticles } = this.props
let { status, user, folders, allArticles, modified, activeArticle } = this.props
let { targetFolders } = status
if (targetFolders == null) targetFolders = []
let modifiedElements = modified.map(modifiedArticle => {
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
if (originalArticle == null) return false
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
let className = 'ArticleNavigator-unsaved-list-item'
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
return (
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
<div className='ArticleNavigator-unsaved-list-item-label'>
<ModeIcon mode={combinedArticle.mode}/>&nbsp;
{combinedArticle.title.trim().length > 0
? combinedArticle.title
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
</div>
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
</div>
)
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
let hasModified = modifiedElements.length > 0
let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key).length
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
@@ -104,9 +141,9 @@ export default class ArticleNavigator extends React.Component {
})
return (
<div className='ArticleNavigator'>
<div tabIndex='1' className='ArticleNavigator'>
<div className='userInfo'>
<div className='userProfileName'>{userName}</div>
<div className='userProfileName'>{user.name}</div>
<div className='userName'>localStorage</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
<i className='fa fa-fw fa-chevron-down'/>
@@ -117,26 +154,26 @@ export default class ArticleNavigator extends React.Component {
</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>
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
<div className='ArticleNavigator-unsaved-list'>
{modifiedElements}
</div>
<div className='ArticleNavigator-unsaved-control'>
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
</div>
</div>*/}
{status.isTutorialOpen ? newPostTutorialElement : null}
</div>
<div className='folders'>
<div className='header'>
<div className={'ArticleNavigator-folders expand'}>
{status.isTutorialOpen ? newFolderTutorialElement : null}
<div className='ArticleNavigator-folders-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>
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
</button>
{status.isTutorialOpen ? newFolderTutorialElement : null}
</div>
<div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
@@ -149,12 +186,17 @@ export default class ArticleNavigator extends React.Component {
}
ArticleNavigator.propTypes = {
activeUser: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
dispatch: PropTypes.func,
status: PropTypes.shape({
folderId: PropTypes.number
}),
dispatch: PropTypes.func
user: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape({
key: PropTypes.string
})
}

View File

@@ -1,31 +1,51 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ExternalLink from 'boost/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial } from 'boost/actions'
import ExternalLink from 'browser/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial, saveArticle, switchFolder } from '../actions'
import { isModalOpen } from 'browser/lib/modal'
import keygen from 'browser/lib/keygen'
import activityRecord from 'browser/lib/activityRecord'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
const OSX = global.process.platform === 'darwin'
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'>
<svg width='750' height='300' className='tutorial'>
<text x='125' y='63' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text>
<text x='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : /{folder_name}\n'}</text>
<text x='465' y='105' fill={BRAND_COLOR} fontSize='14'>
<text x='140' y='135' 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 x='90' width='500' height='300'>
<path fill='white' d='M27.2,6.9c-1.7,3.5-6,4.8-8,8.2c-1.8,3.1-2.1,6.8-1.8,10.2c0.7,7,4.2,16.7,10.3,20.7c0.5,0.4,1.4,0.2,1.8-0.2
c0.1-0.1,0.2-0.2,0.3-0.3c0.6-0.6,0.6-1.5,0-2.1c-0.2-0.2-0.3-0.4-0.5-0.5c-1.3-1.4-3.2,0.7-1.9,2.1c0.2,0.2-0.3,0.4,0.7,0.5
c0-0.7,0-1.4,0-2.1c0,0.1-0.4,0.2-0.5,0.3c0.6-0.1,1.1-0.2,1.7-0.2c-5.7-3.7-9.2-14.5-9-20.9c0.1-4,1.6-6.7,4.8-9.1
c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
<path fill='white' d='M9.5,24.4c2.4-2.7,4.9-5.4,7.3-8c2.5-2.8,5.7-7.6,9.9-7.8c-0.5-0.5-1-1-1.5-1.5c0.1,6.8,1.9,13.1,5.3,18.9
c1,1.7,3.6,0.2,2.6-1.5c-3.2-5.4-4.8-11.1-4.9-17.4c0-0.8-0.7-1.5-1.5-1.5c-3.6,0.2-5.9,2.1-8.3,4.7c-3.7,3.9-7.3,8-11,12
C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
</svg>
</svg>
)
const newPostTutorialElement = (
<svg width='900' height='900' className='tutorial'>
<text x='470' y='50' fill={BRAND_COLOR} fontSize='24'>Create a new post!!</text>
<text x='490' y='75' fill={BRAND_COLOR} fontSize='16' children={`press \`${OSX ? '⌘' : '^'} + n\``}/>
<svg x='415' y='20' width='400' height='400'>
<path fill='white' d='M11.6,14.7c1,5.5,2.9,10.7,5.7,15.5c1,1.7,3.5,0.2,2.6-1.5c-2.6-4.7-4.4-9.6-5.4-14.8
C14.1,12,11.3,12.8,11.6,14.7L11.6,14.7z'/>
<path fill='white' d='M16.8,17.1c4,0.2,7.6-1.1,10.7-3.6c1.5-1.2-0.6-3.3-2.1-2.1c-2.4,2-5.4,2.9-8.6,2.7C14.9,14,14.9,17,16.8,17.1
L16.8,17.1z'/>
<path fill='white' d='M13.8,17.6c11.9,3.5,24.1,4.9,36.4,3.9c1.9-0.1,1.9-3.1,0-3c-12.1,0.9-24-0.3-35.6-3.8
C12.7,14.1,11.9,17,13.8,17.6L13.8,17.6z'/>
</svg>
</svg>
)
@@ -34,19 +54,55 @@ export default class ArticleTopBar extends React.Component {
constructor (props) {
super(props)
this.saveAllHandler = e => {
if (isModalOpen()) return true
this.handleSaveAllButtonClick(e)
}
this.focusSearchHandler = e => {
if (isModalOpen()) return true
this.focusInput(e)
}
this.newPostHandler = e => {
if (isModalOpen()) return true
this.handleNewPostButtonClick(e)
}
this.state = {
isTooltipHidden: true
isTooltipHidden: true,
isLinksDropdownOpen: false
}
}
componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput)
this.linksButton = ReactDOM.findDOMNode(this.refs.links)
this.showLinksDropdown = e => {
e.preventDefault()
e.stopPropagation()
if (!this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: true})
}
}
this.linksButton.addEventListener('click', this.showLinksDropdown)
this.hideLinksDropdown = e => {
if (this.state.isLinksDropdownOpen) {
this.setState({isLinksDropdownOpen: false})
}
}
document.addEventListener('click', this.hideLinksDropdown)
// ipc.on('top-save-all', this.saveAllHandler)
ipc.on('top-focus-search', this.focusSearchHandler)
ipc.on('top-new-post', this.newPostHandler)
}
componentWillUnmount () {
this.searchInput.removeEventListener('keydown', this.showTooltip)
this.searchInput.removeEventListener('focus', this.showTooltip)
this.searchInput.removeEventListener('blur', this.showTooltip)
document.removeEventListener('click', this.hideLinksDropdown)
this.linksButton.removeEventListener('click', this.showLinksDropdown())
// ipc.removeListener('top-save-all', this.saveAllHandler)
ipc.removeListener('top-focus-search', this.focusSearchHandler)
ipc.removeListener('top-new-post', this.newPostHandler)
}
handleTooltipRequest (e) {
@@ -67,7 +123,6 @@ export default class ArticleTopBar extends React.Component {
dispatch(clearSearch())
return
}
this.blurInput()
}
focusInput () {
@@ -90,6 +145,32 @@ export default class ArticleTopBar extends React.Component {
this.focusInput()
}
handleNewPostButtonClick (e) {
let { dispatch, folders, status } = this.props
let { targetFolders } = status
let isFolderFilterApplied = targetFolders.length > 0
let FolderKey = isFolderFilterApplied
? targetFolders[0].key
: folders[0].key
let newArticle = {
key: keygen(),
title: '',
content: '',
mode: 'markdown',
tags: [],
FolderKey: FolderKey,
craetedAt: new Date(),
updatedAt: new Date()
}
dispatch(saveArticle(newArticle.key, newArticle, true))
if (isFolderFilterApplied) dispatch(switchFolder(targetFolders[0].name))
remote.getCurrentWebContents().send('detail-title')
activityRecord.emit('ARTICLE_CREATE')
}
handleTutorialButtonClick (e) {
let { dispatch } = this.props
@@ -99,9 +180,9 @@ export default class ArticleTopBar extends React.Component {
render () {
let { status } = this.props
return (
<div className='ArticleTopBar'>
<div className='left'>
<div className='search'>
<div tabIndex='2' className='ArticleTopBar'>
<div className='ArticleTopBar-left'>
<div className='ArticleTopBar-left-search'>
<i className='fa fa-search fa-fw' />
<input
ref='searchInput'
@@ -114,25 +195,49 @@ export default class ArticleTopBar extends React.Component {
/>
{
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>
? <button onClick={e => this.handleSearchClearButton(e)} className='ArticleTopBar-left-search-clear-button'><i className='fa fa-times'/></button>
: null
}
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
- Search by tag : #{'{string}'}<br/>
- Search by folder : /{'{folder_name}'}
<ul>
<li>- Search by tag : #{'{string}'}</li>
<li>- Search by folder : /{'{folder_name}'}<br/><small>exact match : //{'{folder_name}'}</small></li>
<li>- Only unsaved : --unsaved</li>
</ul>
</div>
</div>
{status.isTutorialOpen ? searchTutorialElement : null}
<div className={'ArticleTopBar-left-control'}>
<button className='ArticleTopBar-left-control-new-post-button' onClick={e => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus'/>
<span className='tooltip'>New Post ({OSX ? '⌘' : '^'} + n)</span>
</button>
{status.isTutorialOpen ? newPostTutorialElement : null}
</div>
</div>
<div className='right'>
<div className='ArticleTopBar-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>
<a ref='links' className='ArticleTopBar-right-links-button' href>
<img src='../resources/app.png' width='44' height='44'/>
</a>
{
this.state.isLinksDropdownOpen
? (
<div className='ArticleTopBar-right-links-button-dropdown'>
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://b00st.io'>
<i className='fa fa-fw fa-home'/>Boost official page
</ExternalLink>
<ExternalLink className='ArticleTopBar-right-links-button-dropdown-item' href='https://github.com/BoostIO/Boostnote/issues'>
<i className='fa fa-fw fa-github'/> Issues
</ExternalLink>
</div>
)
: null
}
</div>
{status.isTutorialOpen ? (
@@ -140,7 +245,7 @@ export default class ArticleTopBar extends React.Component {
<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>
<text x='150' y='55' fontSize='18' fill={BRAND_COLOR} children={'with pressing ' + (OSX ? '`⌘ + Alt + s`' : '`Win + Alt + s`')}/>
</svg>
<svg width='450' className='global'>
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text>
@@ -156,9 +261,9 @@ export default class ArticleTopBar extends React.Component {
}
ArticleTopBar.propTypes = {
search: PropTypes.string,
dispatch: PropTypes.func,
status: PropTypes.shape({
search: PropTypes.string
})
}),
folders: PropTypes.array
}

View File

@@ -1,19 +1,8 @@
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'
import ProfileImage from 'browser/components/ProfileImage'
export default class UserNavigator extends Component {
handleClick (e) {
openModal(CreateNewTeam)
}
// for dev
componentDidMount () {
// openModal(CreateNewTeam)
}
renderUserList () {
if (this.props.users == null) return null
@@ -38,7 +27,7 @@ export default class UserNavigator extends Component {
return (
<div className='UserNavigator'>
{this.renderUserList()}
<button className='createTeamBtn' onClick={e => this.handleClick(e)}>
<button className='createTeamBtn'>
+
<div className='tooltip'>Create a new team</div>
</button>

View File

@@ -1,20 +1,24 @@
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 ReactDOM from 'react-dom'
import { toggleTutorial } from '../actions'
import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './ArticleTopBar'
import ArticleList from './ArticleList'
import ArticleDetail from './ArticleDetail'
import _ from 'lodash'
import keygen from 'boost/keygen'
import { isModalOpen, closeModal } from 'boost/modal'
import { isModalOpen, closeModal } from 'browser/lib/modal'
const electron = require('electron')
const remote = electron.remote
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
const OSX = global.process.platform === 'darwin'
class HomePage extends React.Component {
componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、
@@ -29,12 +33,16 @@ class HomePage extends React.Component {
handleKeyDown (e) {
if (isModalOpen()) {
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
remote.getCurrentWebContents().send('modal-confirm')
}
if (e.keyCode === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { nav, top, list, detail } = this.refs
let { top, list } = this.refs
let listElement = ReactDOM.findDOMNode(list)
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
@@ -42,95 +50,66 @@ class HomePage extends React.Component {
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (top.isInputFocused() && !e.metaKey) {
if (e.keyCode === 13 || e.keyCode === 27) top.escape()
if (e.keyCode === 13 && top.isInputFocused()) {
listElement.focus()
return
}
if (e.keyCode === 27 && top.isInputFocused()) {
if (status.search.length > 0) top.escape()
else listElement.focus()
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()
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (e.keyCode === 27) {
if (document.activeElement !== listElement) {
listElement.focus()
} else {
top.focusInput()
}
return
}
}
render () {
let { dispatch, status, articles, allArticles, activeArticle, folders, tags, filters } = this.props
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
folders={folders}
status={status}
user={user}
folders={folders}
allArticles={allArticles}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
folders={folders}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
status={status}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
activeArticle={activeArticle}
folders={folders}
status={status}
tags={tags}
filters={filters}
user={user}
folders={folders}
modified={modified}
activeArticle={activeArticle}
/>
</div>
)
@@ -139,7 +118,7 @@ class HomePage extends React.Component {
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/) && !key.match(/^--/)
}
// Build filter object by key
@@ -156,12 +135,26 @@ function buildFilter (key) {
return {type: TEXT_FILTER, value: key}
}
function remap (state) {
let { folders, articles, status } = state
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) {
let { user, folders, status } = state
let _articles = state.articles
let articles = _articles != null ? _articles.data : []
let modified = _articles != null ? _articles.modified : []
if (articles == null) articles = []
articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt)
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
if (match === 0) match = b.title.localeCompare(a.title)
if (match === 0) match = b.key.localeCompare(a.key)
return match
})
let allArticles = articles.slice()
@@ -170,6 +163,7 @@ function remap (state) {
return sum.concat(article.tags)
}, []))
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
// Filter articles
let filters = status.search.split(' ')
.map(key => key.trim())
@@ -184,10 +178,10 @@ function remap (state) {
let targetFolders
if (folders != null) {
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 => {
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)
@@ -200,7 +194,7 @@ function remap (state) {
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'))
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
@@ -208,7 +202,7 @@ function remap (state) {
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')))
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
@@ -218,74 +212,30 @@ function remap (state) {
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 {
user,
folders,
status,
allArticles,
articles,
allArticles,
modified,
activeArticle,
tags,
filters: {
folder: folderFilters,
tag: tagFilters,
text: textFilters
}
tags
}
}
HomePage.propTypes = {
params: PropTypes.shape({
userId: PropTypes.string
}),
status: PropTypes.shape({
userId: PropTypes.string
status: PropTypes.shape(),
user: PropTypes.shape({
name: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
filters: PropTypes.shape({
folder: PropTypes.array,
tag: PropTypes.array,
text: PropTypes.array
})
tags: PropTypes.array
}
export default connect(remap)(HomePage)

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
})
}

View File

@@ -1,7 +1,7 @@
import ipc from 'ipc'
const electron = require('electron')
const ipc = electron.ipcRenderer
import React, { PropTypes } from 'react'
var ContactModal = require('boost/components/modal/ContactModal')
import HomePage from './HomePage'
export default class MainContainer extends React.Component {
constructor (props) {
@@ -19,21 +19,31 @@ export default class MainContainer extends React.Component {
ipc.send('update-app', 'Deal with it.')
}
openContactModal () {
this.openModal(ContactModal)
handleWheel (e) {
if (e.ctrlKey && global.process.platform !== 'darwin') {
if (window.document.body.style.zoom == null) {
window.document.body.style.zoom = 1
}
let zoom = Number(window.document.body.style.zoom)
if (e.deltaY > 0 && zoom < 4) {
document.body.style.zoom = zoom + 0.05
} else if (e.deltaY < 0 && zoom > 0.5) {
document.body.style.zoom = zoom - 0.05
}
}
}
render () {
return (
<div className='Main'>
<div
className='Main'
onWheel={(e) => this.handleWheel(e)}
>
{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}
<HomePage/>
</div>
)
}

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

@@ -1,30 +1,71 @@
// Action types
export const USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const LOCK_STATUS = 'LOCK_STATUS'
export const UNLOCK_STATUS = 'UNLOCK_STATUS'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Status - mode
export const IDLE_MODE = 'IDLE_MODE'
export const CREATE_MODE = 'CREATE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Article status
export const NEW = 'NEW'
export function updateUser (input) {
return {
type: USER_UPDATE,
data: input
}
}
// DB
export function cacheArticle (key, article) {
return {
type: ARTICLE_CACHE,
data: { key, article }
}
}
export function uncacheArticle (key) {
return {
type: ARTICLE_UNCACHE,
data: { key }
}
}
export function uncacheAllArticles () {
return {
type: ARTICLE_UNCACHE_ALL
}
}
export function saveArticle (key, article, forceSwitch) {
return {
type: ARTICLE_SAVE,
data: { key, article, forceSwitch }
}
}
export function saveAllArticles () {
return {
type: ARTICLE_SAVE_ALL
}
}
export function updateArticle (article) {
return {
type: ARTICLE_UPDATE,
@@ -77,17 +118,12 @@ export function switchFolder (folderName) {
}
}
export function switchMode (mode) {
return {
type: SWITCH_MODE,
data: mode
}
}
export function switchArticle (articleKey) {
return {
type: SWITCH_ARTICLE,
data: articleKey
data: {
key: articleKey
}
}
}
@@ -111,20 +147,32 @@ export function clearSearch () {
}
}
export function lockStatus () {
return {
type: LOCK_STATUS
}
}
export function unlockStatus () {
return {
type: UNLOCK_STATUS
}
}
export function toggleTutorial () {
return {
type: TOGGLE_TUTORIAL
}
}
export default {
updateUser,
updateArticle,
destroyArticle,
cacheArticle,
uncacheArticle,
uncacheAllArticles,
saveArticle,
saveAllArticles,
createFolder,
updateFolder,
destroyFolder,
replaceFolder,
switchFolder,
switchArticle,
setSearchFilter,
setTagFilter,
clearSearch,
toggleTutorial
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

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,34 +1,57 @@
import React from 'react'
import { Provider } from 'react-redux'
// import { updateUser } from 'boost/actions'
import { Router, Route, IndexRoute } from 'react-router'
import MainPage from './MainPage'
import HomePage from './HomePage'
// import auth from 'boost/auth'
import store from 'boost/store'
import store from './store'
import React from 'react'
import ReactDOM from 'react-dom'
require('../styles/main/index.styl')
import { openModal } from 'boost/modal'
import Tutorial from 'boost/components/modal/Tutorial'
import activityRecord from 'boost/activityRecord'
import ipc from 'ipc'
import { openModal } from 'browser/lib/modal'
import OSSAnnounceModal from './modal/OSSAnnounceModal'
import activityRecord from 'browser/lib/activityRecord'
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()
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
})
let routes = (
<Route path='/' component={MainPage}>
<IndexRoute name='home' component={HomePage}/>
</Route>
)
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')
ReactDOM.render((
<div>
<Provider store={store}>
<Router>{routes}</Router>
<MainPage/>
</Provider>
</div>
), el, function () {
@@ -36,9 +59,10 @@ ReactDOM.render((
loadingCover.parentNode.removeChild(loadingCover)
let status = JSON.parse(localStorage.getItem('status'))
if (status == null) status = {}
if (!status.introWatched) {
openModal(Tutorial)
status.introWatched = true
if (!status.ossAnnounceWatched) {
openModal(OSSAnnounceModal)
status.ossAnnounceWatched = true
localStorage.setItem('status', JSON.stringify(status))
}
})

View File

@@ -1,8 +1,9 @@
import React, { PropTypes } from 'react'
import linkState from 'boost/linkState'
import { createFolder } from 'boost/actions'
import store from 'boost/store'
import FolderMark from 'boost/components/FolderMark'
import ReactDOM from 'react-dom'
import linkState from 'browser/lib/linkState'
import { createFolder } from '../actions'
import store from '../store'
import FolderMark from 'browser/components/FolderMark'
export default class CreateNewFolder extends React.Component {
constructor (props) {
@@ -15,6 +16,10 @@ export default class CreateNewFolder extends React.Component {
}
}
componentDidMount () {
ReactDOM.findDOMNode(this.refs.folderName).focus()
}
handleCloseButton (e) {
this.props.close()
}
@@ -28,7 +33,6 @@ export default class CreateNewFolder extends React.Component {
name,
color
}
try {
store.dispatch(createFolder(input))
} catch (e) {
@@ -84,7 +88,7 @@ export default class CreateNewFolder extends React.Component {
<div className='title'>Create new folder</div>
<input onKeyDown={e => this.handleKeyDown(e)} className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/>
<input ref='folderName' onKeyDown={e => this.handleKeyDown(e)} className='ipt' type='text' valueLink={this.linkState('name')} placeholder='Enter folder name'/>
<div className='colorSelect'>
{colorElements}
</div>

View File

@@ -0,0 +1,54 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import store from '../store'
import { destroyArticle } from '../actions'
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,29 @@
import React, { PropTypes } from 'react'
import ExternalLink from 'browser/components/ExternalLink'
export default class OSSAnnounceModal extends React.Component {
handleCloseBtnClick (e) {
this.props.close()
}
render () {
return (
<div className='OSSAnnounceModal modal'>
<div className='OSSAnnounceModal-title'>Boostnote has been Open-sourced</div>
<ExternalLink className='OSSAnnounceModal-link' href='https://github.com/BoostIO/Boostnote'>
https://github.com/BoostIO/Boostnote
</ExternalLink>
<button
className='OSSAnnounceModal-closeBtn'
onClick={(e) => this.handleCloseBtnClick(e)}
>Close</button>
</div>
)
}
}
OSSAnnounceModal.propTypes = {
close: PropTypes.func
}

View File

@@ -0,0 +1,232 @@
import React, { PropTypes } from 'react'
import linkState from 'browser/lib/linkState'
import { updateUser } from '../../actions'
import fetchConfig from 'browser/lib/fetchConfig'
const electron = require('electron')
const ipc = electron.ipcRenderer
const remote = electron.remote
const OSX = global.process.platform === 'darwin'
export default class AppSettingTab extends React.Component {
constructor (props) {
super(props)
let keymap = Object.assign({}, remote.getGlobal('keymap'))
let config = Object.assign({}, fetchConfig())
let userName = props.user != null ? props.user.name : null
this.state = {
user: {
name: userName,
alert: null
},
userAlert: null,
keymap: keymap,
keymapAlert: null,
config: config,
configAlert: null
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({keymapAlert: {
type: 'success',
message: 'Successfully done!'
}})
}
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)
}
submitHotKey () {
ipc.send('hotkeyUpdated', this.state.keymap)
}
submitConfig () {
ipc.send('configUpdated', this.state.config)
}
handleSaveButtonClick (e) {
this.submitHotKey()
}
handleConfigSaveButtonClick (e) {
this.submitConfig()
}
handleKeyDown (e) {
if (e.keyCode === 13) {
this.submitHotKey()
}
}
handleConfigKeyDown (e) {
if (e.keyCode === 13) {
this.submitConfig()
}
}
handleDisableDirectWriteClick (e) {
let config = this.state.config
config['disable-direct-write'] = !config['disable-direct-write']
this.setState({
config
})
}
handleNameSaveButtonClick (e) {
let { dispatch } = this.props
dispatch(updateUser({name: this.state.user.name}))
this.setState({
userAlert: {
type: 'success',
message: 'Successfully done!'
}
})
}
render () {
let keymapAlert = this.state.keymapAlert
let keymapAlertElement = keymapAlert != null
? (
<p className={`alert ${keymapAlert.type}`}>
{keymapAlert.message}
</p>
) : null
let userAlert = this.state.userAlert
let userAlertElement = userAlert != null
? (
<p className={`alert ${userAlert.type}`}>
{userAlert.message}
</p>
) : null
return (
<div className='AppSettingTab content'>
<div className='section'>
<div className='sectionTitle'>User's info</div>
<div className='sectionInput'>
<label>User name</label>
<input valueLink={this.linkState('user.name')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleNameSaveButtonClick(e)}>Save</button>
{userAlertElement}
</div>
</div>
<div className='section'>
<div className='sectionTitle'>Text</div>
<div className='sectionInput'>
<label>Editor Font Size</label>
<input valueLink={this.linkState('config.editor-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionInput'>
<label>Editor Font Family</label>
<input valueLink={this.linkState('config.editor-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionMultiSelect'>
<label>Editor Indent Style</label>
<div className='sectionMultiSelect-input'>
type
<select valueLink={this.linkState('config.editor-indent-type')}>
<option value='space'>Space</option>
<option value='tab'>Tab</option>
</select>
size
<select valueLink={this.linkState('config.editor-indent-size')}>
<option value='2'>2</option>
<option value='4'>4</option>
<option value='8'>8</option>
</select>
</div>
</div>
<div className='sectionInput'>
<label>Preview Font Size</label>
<input valueLink={this.linkState('config.preview-font-size')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionInput'>
<label>Preview Font Family</label>
<input valueLink={this.linkState('config.preview-font-family')} onKeyDown={e => this.handleConfigKeyDown(e)} type='text'/>
</div>
<div className='sectionSelect'>
<label>Switching Preview</label>
<select valueLink={this.linkState('config.switch-preview')}>
<option value='blur'>When Editor Blurred</option>
<option value='rightclick'>When Right Clicking</option>
</select>
</div>
{
global.process.platform === 'win32'
? (
<div className='sectionCheck'>
<label><input onClick={e => this.handleDisableDirectWriteClick(e)} checked={this.state.config['disable-direct-write']} disabled={OSX} type='checkbox'/>Disable Direct Write<span className='sectionCheck-warn'>It will be applied after restarting</span></label>
</div>
)
: null
}
<div className='sectionConfirm'>
<button onClick={e => this.handleConfigSaveButtonClick(e)}>Save</button>
</div>
</div>
<div className='section'>
<div className='sectionTitle'>Hotkey</div>
<div className='sectionInput'>
<label>Toggle Main</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleMain')} type='text'/>
</div>
<div className='sectionInput'>
<label>Toggle Finder(popup)</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('keymap.toggleFinder')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{keymapAlertElement}
</div>
<div className='description'>
<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>
)
}
}
AppSettingTab.prototype.linkState = linkState
AppSettingTab.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,27 @@
import React from 'react'
import ReactDOM from 'react-dom'
import linkState from 'browser/lib/linkState'
import ExternalLink from 'browser/components/ExternalLink'
export default class ContactTab extends React.Component {
componentDidMount () {
let titleInput = ReactDOM.findDOMNode(this.refs.title)
if (titleInput != null) titleInput.focus()
}
render () {
return (
<div className='ContactTab content'>
<div className='title'>Contact</div>
<p>
- E-mail: <ExternalLink href='mailto:rokt33r@gmail.com?Subject=About%20Boost'>rokt33r@gmail.com</ExternalLink>
</p>
<p>
- Issues: <ExternalLink href='https://github.com/BoostIO/Boostnote/issues'>https://github.com/BoostIO/Boostnote/issues</ExternalLink>
</p>
</div>
)
}
}
ContactTab.prototype.linkState = linkState

View File

@@ -1,8 +1,8 @@
import React, { PropTypes } from 'react'
import linkState from 'boost/linkState'
import FolderMark from 'boost/components/FolderMark'
import store from 'boost/store'
import { updateFolder, destroyFolder, replaceFolder } from 'boost/actions'
import linkState from 'browser/lib/linkState'
import FolderMark from 'browser/components/FolderMark'
import store from '../../store'
import { updateFolder, destroyFolder, replaceFolder } from '../../actions'
const IDLE = 'IDLE'
const EDIT = 'EDIT'

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react'
import FolderRow from './FolderRow'
import linkState from 'boost/linkState'
import { createFolder } from 'boost/actions'
import linkState from 'browser/lib/linkState'
import { createFolder } from '../../actions'
export default class FolderSettingTab extends React.Component {
constructor (props) {
@@ -20,8 +20,6 @@ export default class FolderSettingTab extends React.Component {
handleSaveButtonClick (e) {
this.setState({alert: null}, () => {
if (this.state.name.trim().length === 0) return false
let { dispatch } = this.props
try {

View File

@@ -0,0 +1,121 @@
import React, { PropTypes } from 'react'
import { connect, Provider } from 'react-redux'
import linkState from 'browser/lib/linkState'
import store from '../store'
import AppSettingTab from './Preference/AppSettingTab'
import FolderSettingTab from './Preference/FolderSettingTab'
import ContactTab from './Preference/ContactTab'
import { closeModal } from 'browser/lib/modal'
const APP = 'APP'
const HELP = 'HELP'
const FOLDER = 'FOLDER'
const CONTACT = 'CONTACT'
class Preferences extends React.Component {
constructor (props) {
super(props)
this.state = {
currentTab: APP
}
}
switchTeam (teamId) {
this.setState({currentTeamId: teamId})
}
handleNavButtonClick (tab) {
return e => {
this.setState({currentTab: tab})
}
}
render () {
let content = this.renderContent()
let tabs = [
{target: APP, label: 'Preferences'},
{target: FOLDER, label: 'Manage folder'},
{target: CONTACT, label: 'Contact form'}
]
let navButtons = tabs.map(tab => (
<button key={tab.target} onClick={e => this.handleNavButtonClick(tab.target)(e)} className={this.state.currentTab === tab.target ? 'active' : ''}>{tab.label}</button>
))
return (
<div className='Preferences modal'>
<div className='header'>
<div className='title'>Setting</div>
<button onClick={e => closeModal()} className='closeBtn'>Done</button>
</div>
<div className='nav'>
{navButtons}
</div>
{content}
</div>
)
}
renderContent () {
let { user, folders, dispatch } = this.props
switch (this.state.currentTab) {
case HELP:
return (<HelpTab/>)
case FOLDER:
return (
<FolderSettingTab
dispatch={dispatch}
folders={folders}
/>
)
case CONTACT:
return (
<ContactTab/>
)
case APP:
default:
return (
<AppSettingTab
user={user}
dispatch={dispatch}
/>
)
}
}
}
Preferences.propTypes = {
user: PropTypes.shape({
name: PropTypes.string
}),
folders: PropTypes.array,
dispatch: PropTypes.func
}
Preferences.prototype.linkState = linkState
function remap (state) {
let { user, folders, status } = state
return {
user,
folders,
status
}
}
let RootComponent = connect(remap)(Preferences)
export default class PreferencesModal extends React.Component {
render () {
return (
<Provider store={store}>
<RootComponent/>
</Provider>
)
}
}

View File

@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor'
export default class Tutorial extends React.Component {
constructor (props) {
@@ -80,7 +80,7 @@ export default class Tutorial extends React.Component {
Boost supports code syntax highlighting.<br/>
There are more than 100 different type of language.
<div className='code'>
<CodeEditor readOnly mode='jsx' code={code}/>
<CodeEditor readOnly article={{content:code, mode: 'jsx'}}/>
</div>
</div>
</div>)
@@ -89,12 +89,12 @@ export default class Tutorial extends React.Component {
<div className='title'>Easy to access with Finder</div>
<div className='content'>
The Finder helps you organize all of the files and documents.<br/>
There is a short-cut key [control + shift + tab] to open the Finder.<br/>
There is a short-cut key [ + alt + s] to open the Finder.<br/>
It is available to save your articles on the Clipboard<br/>
by selecting your file with pressing Enter key,<br/>
and to paste the contents of the Clipboard with [Command-V]
and to paste the contents of the Clipboard with [{process.platform === 'darwin' ? 'Command' : 'Control'}-V]
<img width='480' src='../../resources/finder.png'/>
<img width='480' src='../resources/finder.png'/>
</div>
</div>)
case 4:

306
browser/main/reducer.js Normal file
View File

@@ -0,0 +1,306 @@
import { combineReducers } from 'redux'
import _ from 'lodash'
import {
// Status action type
SWITCH_FOLDER,
SWITCH_ARTICLE,
SET_SEARCH_FILTER,
SET_TAG_FILTER,
CLEAR_SEARCH,
TOGGLE_TUTORIAL,
// user
USER_UPDATE,
// Article action type
ARTICLE_UPDATE,
ARTICLE_DESTROY,
ARTICLE_CACHE,
ARTICLE_UNCACHE,
ARTICLE_UNCACHE_ALL,
ARTICLE_SAVE,
ARTICLE_SAVE_ALL,
// Folder action type
FOLDER_CREATE,
FOLDER_UPDATE,
FOLDER_DESTROY,
FOLDER_REPLACE
} from './actions'
import dataStore from 'browser/lib/dataStore'
import keygen from 'browser/lib/keygen'
import activityRecord from 'browser/lib/activityRecord'
const initialStatus = {
search: '',
isTutorialOpen: false
}
dataStore.init()
let data = dataStore.getData()
let initialArticles = {
data: data && data.articles ? data.articles : [],
modified: []
}
let initialFolders = data && data.folders ? data.folders : []
let initialUser = dataStore.getUser().user
function user (state = initialUser, action) {
switch (action.type) {
case USER_UPDATE:
let updated = Object.assign(state, action.data)
dataStore.saveUser(null, updated)
return updated
default:
return state
}
}
function folders (state = initialFolders, action) {
state = state.slice()
switch (action.type) {
case FOLDER_CREATE:
{
let newFolder = action.data.folder
if (!_.isString(newFolder.name)) throw new Error('Folder name must be a string')
newFolder.name = newFolder.name.trim().replace(/\s/g, '_')
Object.assign(newFolder, {
key: keygen(),
createdAt: new Date(),
updatedAt: new Date()
})
if (newFolder.name == null || newFolder.name.length === 0) throw new Error('Folder name is required')
if (newFolder.name.match(/\//)) throw new Error('`/` is not available for folder name')
let conflictFolder = _.find(state, folder => folder.name.toLowerCase() === newFolder.name.toLowerCase())
if (conflictFolder != null) throw new Error(`${conflictFolder.name} already exists!`)
state.push(newFolder)
dataStore.setFolders(state)
activityRecord.emit('FOLDER_CREATE')
return state
}
case FOLDER_UPDATE:
{
let folder = action.data.folder
let targetFolder = _.findWhere(state, {key: folder.key})
if (!_.isString(folder.name)) throw new Error('Folder name must be a string')
folder.name = folder.name.trim().replace(/\s/g, '_')
if (folder.name.length === 0) throw new Error('Folder name is required')
if (folder.name.match(/\//)) throw new Error('`/` is not available for folder name')
// Folder existence check
if (targetFolder == null) throw new Error('Folder doesnt exist')
// Name conflict check
if (targetFolder.name !== folder.name) {
let conflictFolder = _.find(state, _folder => {
return folder.name.toLowerCase() === _folder.name.toLowerCase() && folder.key !== _folder.key
})
if (conflictFolder != null) throw new Error('Name conflicted')
}
Object.assign(targetFolder, folder, {
updatedAt: new Date()
})
dataStore.setFolders(state)
activityRecord.emit('FOLDER_UPDATE')
return state
}
case FOLDER_DESTROY:
{
if (state.length < 2) throw new Error('Folder must exist more than one')
let targetKey = action.data.key
let targetIndex = _.findIndex(state, folder => folder.key === targetKey)
if (targetIndex >= 0) {
state.splice(targetIndex, 1)
}
dataStore.setFolders(state)
activityRecord.emit('FOLDER_DESTROY')
return state
}
case FOLDER_REPLACE:
{
let { a, b } = action.data
let folderA = state[a]
let folderB = state[b]
state.splice(a, 1, folderB)
state.splice(b, 1, folderA)
}
dataStore.setFolders(state)
return state
default:
return state
}
}
function compareArticle (original, modified) {
var keys = _.keys(_.pick(modified, ['mode', 'title', 'tags', 'content', 'FolderKey']))
return keys.reduce((sum, key) => {
if ((key === 'tags' && !_.isEqual(original[key], modified[key])) || (key !== 'tags' && original[key] !== modified[key])) {
if (sum == null) {
sum = {
key: original.key
}
}
sum[key] = modified[key]
}
return sum
}, null)
}
function articles (state = initialArticles, action) {
switch (action.type) {
case ARTICLE_CACHE:
{
let modified = action.data.article
let targetKey = action.data.key
let originalIndex = _.findIndex(state.data, _article => targetKey === _article.key)
if (originalIndex === -1) return state
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
modified = compareArticle(state.data[originalIndex], modified)
if (modified == null) {
if (modifiedIndex !== -1) state.modified.splice(modifiedIndex, 1)
return state
}
if (modifiedIndex === -1) state.modified.push(modified)
else Object.assign(state.modified[modifiedIndex], modified)
return state
}
case ARTICLE_UNCACHE:
{
let targetKey = action.data.key
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
return state
}
case ARTICLE_UNCACHE_ALL:
state.modified = []
return state
case ARTICLE_SAVE:
{
let targetKey = action.data.key
let override = action.data.article
let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
let modified = modifiedIndex !== -1 ? state.modified.splice(modifiedIndex, 1)[0] : null
let targetIndex = _.findIndex(state.data, _article => targetKey === _article.key)
// Make a new if target article is not found.
if (targetIndex === -1) {
state.data.push(Object.assign({
title: '',
content: '',
mode: 'markdown',
tags: [],
craetedAt: new Date()
}, modified, override, {key: targetKey, updatedAt: new Date()}))
return state
}
Object.assign(state.data[targetIndex], modified, override, {key: targetKey, updatedAt: new Date()})
dataStore.setArticles(state.data)
return state
}
case ARTICLE_SAVE_ALL:
if (state.modified.length > 0) {
state.modified.forEach(modifiedArticle => {
let targetIndex = _.findIndex(state.data, _article => modifiedArticle.key === _article.key)
Object.assign(state.data[targetIndex], modifiedArticle, {key: modifiedArticle.key, updatedAt: new Date()})
})
}
state.modified = []
dataStore.setArticles(state.data)
return state
case ARTICLE_UPDATE:
{
let article = action.data.article
let targetIndex = _.findIndex(state.data, _article => article.key === _article.key)
if (targetIndex < 0) state.data.unshift(article)
else Object.assign(state.data[targetIndex], article)
dataStore.setArticles(state.data)
return state
}
case ARTICLE_DESTROY:
{
let articleKey = action.data.key
let targetIndex = _.findIndex(state.data, _article => articleKey === _article.key)
if (targetIndex >= 0) state.data.splice(targetIndex, 1)
let modifiedIndex = _.findIndex(state.modified, _article => articleKey === _article.key)
if (modifiedIndex >= 0) state.modified.splice(modifiedIndex, 1)
dataStore.setArticles(state.data)
return state
}
case FOLDER_DESTROY:
{
let folderKey = action.data.key
state.data = state.data.filter(article => article.FolderKey !== folderKey)
dataStore.setArticles(state.data)
return state
}
default:
return state
}
}
function status (state = initialStatus, action) {
state = Object.assign({}, state)
switch (action.type) {
case TOGGLE_TUTORIAL:
state.isTutorialOpen = !state.isTutorialOpen
return state
}
switch (action.type) {
case ARTICLE_SAVE:
if (action.data.forceSwitch) {
let article = action.data.article
state.articleKey = article.key
state.search = ''
}
return state
case SWITCH_FOLDER:
state.search = `\/\/${action.data} `
return state
case SWITCH_ARTICLE:
state.articleKey = action.data.key
return state
case SET_SEARCH_FILTER:
state.search = action.data
return state
case SET_TAG_FILTER:
state.search = `#${action.data}`
return state
case CLEAR_SEARCH:
state.search = ''
return state
default:
return state
}
}
export default combineReducers({
user,
folders,
articles,
status
})

View File

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

View File

@@ -0,0 +1,391 @@
noTagsColor = #999
infoButton()
display inline-block
border-radius 16.5px
cursor pointer
height 33px
width 33px
line-height 33px
margin-right 5px
font-size 18px
color inactiveTextColor
background-color white
padding 0
border 1px solid white
&:focus
border-color focusBorderColor
&:hover
color inherit
.ArticleDetail
absolute right bottom
top 60px
left 450px
padding 10px
background-color #E6E6E6
border-top 1px solid borderColor
border-left 1px solid borderColor
&.empty
.ArticleDetail-empty-box
line-height 72px
font-size 42px
height 320px
display flex
align-items center
.ArticleDetail-empty-box-message
text-align center
width 100%
color inactiveTextColor
.ArticleDetail-info
height 70px
width 100%
font-size 12px
user-select none
&>.tutorial
position fixed
z-index 35
.ArticleDetail-info-folder
display inline-block
max-width 100px
overflow ellipsis
height 10px
width 150px
height 27px
outline none
background-color darken(white, 5%)
border 1px solid transparent
&:hover
background-color white
&:focus
border-color focusBorderColor
&>.tutorial
position fixed
z-index 35
.ArticleDetail-info-status
padding 0 5px
.unsaved-mark
color brandColor
.ArticleDetail-info-control
float right
clearfix
.ShareButton
display block
float left
&>button, .ShareButton-open-button
infoButton()
.tooltip
tooltip()
margin-top 30px
&:hover
.tooltip
opacity 1
&>button
float left
&:nth-child(1) .tooltip
margin-left -65px
.ArticleDetail-info-control-delete-button
.tooltip
right 5px
.ArticleDetail-info-control-save
float left
width 80px
margin-right 5px
overflow hidden
transition width 0.15s ease-in-out
border-radius 16.5px
&.hide
width 0px
opacity 0.2
.ArticleDetail-info-control-save-button
infoButton()
background-color brandColor
color white
font-size 12px
width 100%
border 1px solid brandBorderColor
white-space nowrap
.fa
font-size 18px
&:hover
color white
background-color lighten(brandColor, 15%)
&:focus
color white
background-color lighten(brandColor, 15%)
.tooltip
tooltip()
margin-top 30px
margin-left -90px
&:hover .tooltip
opacity 1
.ShareButton-open-button .tooltip
margin-left -40px
.ShareButton-dropdown
position fixed
width 185px
z-index 35
background-color #F0F0F0
padding 4px 0
border-radius 5px
right 5px
top 95px
box-shadow 0px 0px 10px 1px alpha(#bbb, 0.8)
border 1px solid #bcbcbc
&.hide
display none
&>button
background-color transparent
height 21px
width 100%
border none
padding-left 20px
text-align left
font-size 13px
font-family '.HelveticaNeueDeskInterface-Regular', sans-serif
&:hover
background-color #4297FE
color white
.ShareButton-url
height 40px
width 100%
position relative
padding 0 5px
.ShareButton-url-input
height 21px
border none
width 143px
float left
border-top-left-radius 3px
border-bottom-left-radius 3px
border 1px solid borderColor
border-right none
.ShareButton-url-button
height 21px
border none
width 30px
float left
background-color #F0F0F0
border-top-right-radius 3px
border-bottom-right-radius 3px
border 1px solid borderColor
.ShareButton-url-button-tooltip
tooltip()
right 10px
margin-top 5px
&:hover
.ShareButton-url-button-tooltip
opacity 1
&:active
background-color #4297FE
color white
.ShareButton-url-alert
padding 10px
line-height 16px
.ArticleDetail-info-row2
.tutorial
position fixed
z-index 35
font-style italic
.TagSelect
margin-top 5px
.TagSelect-tags
white-space nowrap
overflow-x auto
position relative
noSelect()
z-index 30
background-color #E6E6E6
clearfix()
.TagSelect-tags-item
background-color transparent
color white
margin 0 2px
padding 0
height 17px
float left
button.TagSelect-tags-item-remove
display block
float left
background-color transparent
border none
font-size 8px
color white
width 15px
height 17px
text-align center
line-height 12px
padding 0
margin 0
border-top solid 1px darken(brandColor, 5%)
border-bottom solid 1px darken(brandColor, 5%)
border-left solid 1px darken(brandColor, 5%)
border-right solid 1px transparent
border-radius left 2px
background-color brandColor
&:hover
background-color lighten(brandColor, 10%)
border-color lighten(brandColor, 10%)
&:focus
background-color lighten(brandColor, 10%)
border-color focusBorderColor
.TagSelect-tags-item-label
background-color brandColor
float left
font-size 12px
border-top solid 1px darken(brandColor, 5%)
border-bottom solid 1px darken(brandColor, 5%)
border-right solid 1px darken(brandColor, 5%)
line-height 15px
padding 0 5px
border-radius right 2px
input.TagSelect-input
background-color transparent
border none
border-bottom 1px solid transparent
outline none
margin 0 2px
transition 0.15s
height 18px
&:focus
border-color focusBorderColor
.TagSelect-suggest
position fixed
width 150px
max-height 150px
background-color white
z-index 50
border 1px solid borderColor
border-radius 5px
overflow-y auto
&>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%)
.ArticleDetail-panel
position absolute
top 70px
left 10px
right 10px
bottom 10px
overflow-x hidden
overflow-y auto
background-color white
border-radius 5px
border solid 1px lighten(borderColor, 15%)
&>.ArticleDetail-panel-header
display block
height 60px
&>.tutorial
fixed right
z-index 35
font-style italic
.ArticleDetail-panel-header-mode
z-index 30
background-color white
absolute top bottom
right 10px
display block
height 33px
margin-top 14px
width 120px
margin-right 15px
border solid 1px borderColor
border-radius 5px
transition width 0.15s
user-select none
&.idle
cursor pointer
&:hover
background-color darken(white, 5%)
.ModeIcon
padding 0 5px
line-height 33px
&.edit
border-color focusBorderColor
input
width 120px
line-height 31px
padding 0 10px
border none
outline none
background-color transparent
font-size 14px
.ModeSelect-options
position fixed
width 120px
z-index 10
border 1px solid borderColor
border-radius 5px
background-color white
max-height 250px
overflow-y auto
margin-top 5px
.ModeSelect-options-item
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%)
.ArticleDetail-panel-header-title
absolute left top
right 145px
padding 0 15px
background-color transparent
input
border none
line-height 60px
width 100%
font-size 24px
outline none
.ArticleEditor
absolute left right bottom
top 60px
.ArticleDetail-panel-content-tooltip
absolute bottom right
height 24px
background-color alpha(black, 0.5)
line-height 24px
color white
padding 0 15px
opacity 0
transition 0.1s
z-index 35
&:hover .ArticleDetail-panel-content-tooltip
opacity 1
.MarkdownPreview
absolute top left right bottom
marked()
box-sizing border-box
padding 5px 15px
border-top solid 1px borderColor
overflow-y auto
user-select all
&.empty
color lighten(inactiveTextColor, 10%)
user-select none
font-size 14px
.CodeEditor
absolute top left right bottom
border-top solid 1px borderColor
min-height 300px
border-bottom-left-radius 5px
border-bottom-right-radius 5px

View File

@@ -8,36 +8,42 @@ articleItemColor = #777
width 250px
border-top 1px solid borderColor
border-right 1px solid borderColor
&:focus
border-color focusBorderColor
overflow-y auto
noSelect()
&>div
.articleItem
.ArticleList-item
border solid 2px transparent
position relative
height 88px
min-height 110px
width 100%
cursor pointer
transition 0.1s
background-color white
padding 0 10px
font-size 12px
.top
.ArticleList-item-top
clearfix()
line-height 20px
padding 5px 0
padding-top 2px
line-height 18px
height 20px
color articleItemColor
font-size 11px
.folderName
overflow ellipsis
display inline-block
width 120px
.updatedAt
float right
line-height 20px
.middle
padding 3px 0 7px
line-height 18px
.unsaved-mark
color brandColor
.ArticleList-item-middle
font-size 16px
position relative
height 26px
padding-top 6px
height 22px
.mode
position absolute
left 0
@@ -48,18 +54,41 @@ articleItemColor = #777
left 19px
right 0
overflow ellipsis
.bottom
padding 5px 0
overflow-x auto
white-space nowrap
small
color #AAA
.ArticleList-item-middle2
padding-top 8px
pre
color lighten(inactiveTextColor, 10%)
white-space pre-wrap
overflow hidden
height 33px
line-height 14px
font-size 10px
code
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
.ArticleList-item-bottom
padding-bottom 5px
.tags
color articleItemColor
line-height 18px
word-wrap break-word
clearfix()
i.fa-tags
display inline
float left
padding 2px 2px 0 0
height 14px
line-height 13px
a
background-color brandColor
float left
color white
border-radius 2px
padding 1.5px 5px
padding 1px 5px
margin 2px
height 14px
line-height 13px
font-size 10px
opacity 0.8
&:hover

View File

@@ -0,0 +1,212 @@
articleNavBgColor = #353535
articleCount = #999
.ArticleNavigator
background-color articleNavBgColor
absolute top bottom left
width 200px
border-right 1px solid borderColor
color white
user-select none
.userInfo
height 60px
display block
border-bottom 1px solid borderColor
.userProfileName
color brandColor
font-size 28px
padding 6px 37px 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
.ArticleNavigator-unsaved
position absolute
top 100px
width 100%
height 225px
transition opacity 0.2s ease-in-out
&.hide
opacity 0.2
.ArticleNavigator-unsaved-header
border-bottom 1px solid alpha(borderColor, 0.5)
padding-bottom 5px
clearfix()
position relative
padding-left 10px
font-size 18px
line-height 22px
.ArticleNavigator-unsaved-list
height 165px
padding 5px 0
overflow-y scroll
.ArticleNavigator-unsaved-list-item
height 33px
padding-left 15px
clearfix()
transition 0.1s
cursor pointer
overflow hidden
&:hover
background-color alpha(white, 0.05)
&.active, &:active
background-color alpha(lighten(brandColor, 25%), 70%)
.ArticleNavigator-unsaved-list-item-label
float left
width 151px
line-height 33px
overflow ellipsis
.ArticleNavigator-unsaved-list-item-label-untitled
color inactiveTextColor
.ArticleNavigator-unsaved-list-item-discard-button
float right
width 33px
line-height 30px
height 33px
border none
background-color transparent
color white
font-size 18px
opacity 0.5
&:hover
opacity 1
.ArticleNavigator-unsaved-list-empty
height 33px
padding-left 15px
color alpha(white, 0.4)
transition 0.1s
line-height 33px
&:hover
color alpha(white, 0.6)
.ArticleNavigator-unsaved-control
absolute bottom
height 33px
border-top 1px solid alpha(borderColor, 0.5)
width 100%
.ArticleNavigator-unsaved-control-save-all-button
border none
background-color transparent
font-size 14px
color brandColor
padding-left 15px
width 100%
height 33px
text-align left
&:hover
color lighten(brandColor, 15%)
background-color alpha(white, 0.05)
&:active
color white
&:disabled
color alpha(brandColor, 0.5)
&:hover
color alpha(lighten(brandColor, 25%), 0.5)
background-color transparent
.ArticleNavigator-folders
absolute bottom
top 365px
width 100%
transition top 0.15s ease-in-out
background-color articleNavBgColor
.tutorial
position fixed
z-index 35
font-style italic
&.expand
top 100px
.ArticleNavigator-folders-header
border-bottom 1px solid alpha(borderColor, 0.5)
padding-bottom 5px
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
.folderList
absolute bottom
top 33px
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 alpha(white, 0.05)
&.active, &:active
background-color alpha(lighten(brandColor, 25%), 70%)
.articleCount
color white
.articleCount
color articleCount
font-size 12px

View File

@@ -1,6 +1,5 @@
bgColor = #E6E6E6
inputBgColor = white
iptFocusBorderColor = #369DCD
topBarBtnColor = #B3B3B3
topBarBtnBgColor = #B3B3B3
@@ -15,6 +14,7 @@ infoBtnActiveBgColor = #3A3A3A
left 200px
height 60px
background-color bgColor
user-select none
&>.tutorial
.clickJammer
fixed top left bottom right
@@ -36,14 +36,15 @@ infoBtnActiveBgColor = #3A3A3A
fixed top left bottom right
z-index 20
background-color transparentify(black, 80%)
&>.left
&>.ArticleTopBar-left
float left
&>.tutorial
fixed top
left 200px
left 100px
top 30px
z-index 36
font-style italic
&>.search
&>.ArticleTopBar-left-search
position relative
float left
height 33px
@@ -62,6 +63,17 @@ infoBtnActiveBgColor = #3A3A3A
opacity 1
&.hide
opacity 0
ul
li
line-height 18px
li:last-child
line-height 10px
margin-bottom 3px
small
font-size 10px
position relative
top -2px
margin-left 15px
input
absolute top left
width 350px
@@ -75,7 +87,7 @@ infoBtnActiveBgColor = #3A3A3A
line-height 33px
z-index 0
&:focus
border-color iptFocusBorderColor
border-color focusBorderColor
i.fa.fa-search
position absolute
display block
@@ -84,7 +96,7 @@ infoBtnActiveBgColor = #3A3A3A
line-height 33px
z-index 1
pointer-events none
.searchClearBtn
.ArticleTopBar-left-search-clear-button
position absolute
top 6px
right 10px
@@ -98,24 +110,56 @@ infoBtnActiveBgColor = #3A3A3A
line-height 20px
text-align center
padding 0
&:focus
color textColor
&:hover
color white
background-color topBarBtnBgColor
&>.refreshBtn
&:active
color white
background-color darken(topBarBtnBgColor, 35%)
.ArticleTopBar-left-control
line-height 33px
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
margin-left 20px
.tutorial
fixed top
left 200px
z-index 36
font-style italic
button.ArticleTopBar-left-control-new-post-button
position fixed
background bgColor
font-size 20px
border none
outline none
color inactiveTextColor
width 33px
height 33px
border-radius 16.5px
transition 0.1s
border 1px solid transparent
z-index 30
&:hover
color textColor
&:active
color textColor
background-color lighten(topBarBtnBgColor, 15%)
&:disabled
color inactiveTextColor
background transparent
&:focus
color textColor
.tooltip
tooltip()
margin-left -80px
margin-top 40px
&:hover
.tooltip
opacity 1
&>.ArticleTopBar-right
float right
&>button
display block
@@ -129,28 +173,52 @@ infoBtnActiveBgColor = #3A3A3A
background-color infoBtnBgColor
color bgColor
border-radius 11px
border none
border 1px solid bgColor
transition 0.1s
&:focus
background-color lighten(infoBtnActiveBgColor, 15%)
.tooltip
tooltip()
margin-left -50px
margin-top 29px
margin-top 20px
&:hover
background-color infoBtnActiveBgColor
.tooltip
opacity 1
&>.logo
&>.ArticleTopBar-right-links-button
display block
position absolute
top 8px
right 15px
opacity 0.7
.tooltip
tooltip()
margin-top 44px
margin-left -120px
border-radius 23px
height 46px
width 46px
border 1px solid transparent
&:focus
border-color focusBorderColor
&:hover
opacity 1
.tooltip
opacity 1
&>.ArticleTopBar-right-links-button-dropdown
position fixed
z-index 50
right 10px
top 40px
background-color transparentify(invBackgroundColor, 80%)
padding 5px 0
.ArticleTopBar-right-links-button-dropdown-item
padding 0 10px
height 33px
width 100%
display block
line-height 33px
text-decoration none
color white
&:hover
background-color transparentify(lighten(invBackgroundColor, 30%), 80%)

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,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,12 +0,0 @@
@require './components/UserNavigator'
@require './components/ArticleNavigator'
@require './components/ArticleTopBar'
@require './components/ArticleList'
@require './components/ArticleDetail'
@require './lib/modal'
@require './lib/CreateNewTeam'
@require './lib/CreateNewFolder'
@require './lib/Preferences'
@require './lib/Tutorial'
@require './lib/EditedAlert'

View File

@@ -1,259 +0,0 @@
/**
* React Select
* ============
* Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
* https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
* MIT License: https://github.com/keystonejs/react-select
*/
.Select {
position: relative;
}
.Select-control {
position: relative;
overflow: hidden;
background-color: #ffffff;
border: 1px solid #cccccc;
border-color: #d9d9d9 #cccccc #b3b3b3;
border-radius: 4px;
box-sizing: border-box;
color: #333333;
cursor: default;
outline: none;
padding: 8px 52px 8px 10px;
transition: all 200ms ease;
}
.Select-control:hover {
// box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
.is-searchable.is-open > .Select-control {
cursor: text;
}
.is-open > .Select-control {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
background: #ffffff;
border-color: #b3b3b3 #cccccc #d9d9d9;
}
.is-open > .Select-control > .Select-arrow {
border-color: transparent transparent #999999;
border-width: 0 5px 5px;
}
.is-searchable.is-focused:not(.is-open) > .Select-control {
cursor: text;
}
.is-focused:not(.is-open) > .Select-control {
// border-color: #0088cc #0099e6 #0099e6;
// box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px -1px rgba(0, 136, 204, 0.5);
}
.Select-placeholder {
color: #aaaaaa;
padding: 8px 52px 8px 10px;
position: absolute;
top: 0;
left: 0;
right: -15px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.has-value > .Select-control > .Select-placeholder {
color: #333333;
}
.Select-input > input {
cursor: default;
background: none transparent;
box-shadow: none;
height: auto;
border: 0 none;
font-family: inherit;
font-size: inherit;
margin: 0;
padding: 0;
outline: none;
display: inline-block;
-webkit-appearance: none;
}
.is-focused .Select-input > input {
cursor: text;
}
.Select-control:not(.is-searchable) > .Select-input {
outline: none;
}
.Select-loading {
-webkit-animation: Select-animation-spin 400ms infinite linear;
-o-animation: Select-animation-spin 400ms infinite linear;
animation: Select-animation-spin 400ms infinite linear;
width: 16px;
height: 16px;
box-sizing: border-box;
border-radius: 50%;
border: 2px solid #cccccc;
border-right-color: #333333;
display: inline-block;
position: relative;
margin-top: -8px;
position: absolute;
right: 30px;
top: 50%;
}
.has-value > .Select-control > .Select-loading {
right: 46px;
}
.Select-clear {
color: #999999;
cursor: pointer;
display: inline-block;
font-size: 16px;
padding: 6px 10px;
position: absolute;
right: 17px;
top: 0;
}
.Select-clear:hover {
color: #c0392b;
}
.Select-clear > span {
font-size: 1.1em;
}
.Select-arrow-zone {
content: " ";
display: block;
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 30px;
cursor: pointer;
}
.Select-arrow {
border-color: #999999 transparent transparent;
border-style: solid;
border-width: 5px 5px 0;
content: " ";
display: block;
height: 0;
margin-top: -ceil(2.5px);
position: absolute;
right: 10px;
top: 14px;
width: 0;
cursor: pointer;
}
.Select-menu-outer {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
background-color: #ffffff;
border: 1px solid #cccccc;
border-top-color: #e6e6e6;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
box-sizing: border-box;
margin-top: -1px;
max-height: 200px;
position: absolute;
top: 100%;
width: 100%;
z-index: 1000;
-webkit-overflow-scrolling: touch;
}
.Select-menu {
max-height: 198px;
overflow-y: auto;
}
.Select-option {
box-sizing: border-box;
color: #666666;
cursor: pointer;
display: block;
padding: 8px 10px;
}
.Select-option:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
.Select-option.is-focused {
background-color: #f2f9fc;
color: #333333;
}
.Select-option.is-disabled {
color: #cccccc;
cursor: not-allowed;
}
.Select-noresults {
box-sizing: border-box;
color: #999999;
cursor: default;
display: block;
padding: 8px 10px;
}
.Select.is-multi .Select-control {
padding: 2px 52px 2px 3px;
}
.Select.is-multi .Select-input {
vertical-align: middle;
border: 1px solid transparent;
margin: 2px;
padding: 3px 0;
}
.Select-item {
background-color: brandColor;
border-radius: 2px;
// border: 1px solid #c9e6f2;
color: white;
display: inline-block;
font-size: 1em;
margin: 2px;
}
.Select-item-icon,
.Select-item-label {
display: inline-block;
vertical-align: middle;
}
.Select-item-label {
cursor: default;
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
padding: 3px 5px;
}
.Select-item-label .Select-item-label__a {
color: white;
cursor: white;
}
.Select-item-icon {
cursor: pointer;
border-bottom-left-radius: 2px;
border-top-left-radius: 2px;
border-right: 1px solid darken(brandColor, 10%)
padding: 2px 5px 4px;
}
.Select-item-icon:hover,
.Select-item-icon:focus {
background-color: lighten(brandColor, 10%)
}
.Select-item-icon:active {
background-color: #c9e6f2;
}
.Select.is-multi.is-disabled .Select-item {
background-color: #f2f2f2;
border: 1px solid #d9d9d9;
color: #888888;
}
.Select.is-multi.is-disabled .Select-item-icon {
cursor: not-allowed;
border-right: 1px solid #d9d9d9;
}
.Select.is-multi.is-disabled .Select-item-icon:hover,
.Select.is-multi.is-disabled .Select-item-icon:focus,
.Select.is-multi.is-disabled .Select-item-icon:active {
background-color: #f2f2f2;
}
@keyframes Select-animation-spin {
to {
transform: rotate(1turn);
}
}
@-webkit-keyframes Select-animation-spin {
to {
-webkit-transform: rotate(1turn);
}
}

View File

@@ -1,89 +0,0 @@
.LoginContainer, .SignupContainer
margin 0 auto
padding 105px 15px
box-sizing border-box
color inactiveTextColor
.logo
width 150px
height 150px
display block
margin 0 auto
.authNavigator
margin 15px 0 25px
a
font-size 1.5em
text-decoration none
color inactiveTextColor
&:hover, &.hover, &:active, &.active
color brandColor
.socialControl
text-align center
margin 25px 0
p
margin-bottom 25px
.facebookBtn, .githubBtn
margin 0 45px
width 50px
height 50px
line-height 50px
font-size 25px
text-align center
background-image none
color white
border none
border-radius 25px
cursor pointer
.facebookBtn
background-color facebookColor
&:hover, &.hover
background-color lighten(facebookColor, 25%)
.githubBtn
background-color githubBtn
font-size 30px
line-height 30px
&:hover, &.hover
background-color lighten(githubBtn, 25%)
.divider
.dividerLabel
text-align center
position relative
top -27px
font-size 1.3em
background-color backgroundColor
margin 0 auto
width 50px
form
width 400px
margin 0 auto 45px
.alertInfo, .alertError
margin-top 15px
margin-bottom 15px
padding 10px
border-radius 5px
line-height 1.6
text-align center
.alertInfo
alertInfo()
.alertError
alertError()
div.formField
input
stripInput()
height 33px
width 100%
margin-bottom 10px
text-align center
font-size 1.1em
&:last-child
margin-top 15px
button.logInButton
btnPrimary()
height 44px
border-radius 22px
display block
width 200px
font-size 1em
margin 0 auto
p.alert
text-align center
font-size 0.8em

View File

@@ -1,332 +0,0 @@
navigationWidth= 200px
articleListWidth= 275px
.PlanetContainer
absolute top bottom right left
.tags
white-space: nowrap;
overflow-x: auto;
a
margin 0 2px
text-decoration underline
cursor pointer
font-size 0.95em
&.noTag
color inactiveTextColor
font-size 0.8em
.PlanetHeader
absolute left right top
overflow-y hidden
height 55px
background-color white
border-bottom solid 1px borderColor
box-sizing border-box
padding 5px 15px
clearfix()
.headerLabel
noSelect()
absolute top left bottom
overflow hidden
display inline-block
width navigationWidth
.userName
position absolute
left 15px
top 30px
width 140px
font-size 1em
color textColor
text-decoration none
&:hover
color darken(lightButtonColor, 50%)
text-decoration underline
.planetName
position absolute
top 5px
left 10px
width 145px
font-size 1.6em
color brandColor
overflow hidden
text-overflow ellipsis
white-space nowrap
&:hover
color darken(brandBorderColor, 30%)
.private
position absolute
top 12px
right 38px
width 33px
height 33px
line-height 33px
text-align center
color inactiveColor
&:hover
color textColor
.tooltip
tooltip()
margin-left -30px
&:hover .tooltip
opacity 1
.planetSettingButton
position absolute
top 15px
right 5px
font-size 0.8em
btnDefault()
box-sizing border-box
circle()
width 26px
height 26px
text-align center
cursor pointer
transition 0.1s
&:focus, &.focus
outline none
.tooltip
tooltip()
margin-top 11px
margin-left -36px
&:hover .tooltip
opacity 1
.headerControl
noSelect()
absolute top bottom right
left navigationWidth
.searchInput
display block
position absolute
top 12px
left 0
input
padding-left 32px
width 300px
.fa
position absolute
top 8px
left 12px
color inactiveTextColor
.refreshButton
display block
position absolute
top 15px
right 55px
width 26px
height 26px
font-size 0.8em
btnDefault()
circle()
text-align center
cursor pointer
transition 0.1s
&:focus, &.focus
outline none
.tooltip
tooltip()
margin-top 11px
margin-left -39px
&:hover .tooltip
opacity 1
.logo
display block
position absolute
top 4px
right 10px
cursor pointer
img
transition 0.1s
opacity 0.9
&:hover img, &:hover .tooltip
opacity 1
.tooltip
tooltip()
margin-top -5px
margin-left -67px
.PlanetNavigator
absolute bottom left
noSelect()
top 55px
width navigationWidth
border-right solid 1px highlightenBorderColor
padding 10px
box-sizing border-box
.launchButton
border-radius 22px
font-size 1.1em
nav
a
display block
box-sizing border-box
padding 15px 15px
margin 10px 0
border-radius 10px
text-decoration none
background-color transparent
color textColor
cursor pointer
transition 0.1s
btnDefault()
border none
.PlanetArticleList
absolute bottom right
left navigationWidth
top 55px
width articleListWidth
border-right solid 1px highlightenBorderColor
&>ul
absolute top bottom left right
overflow-y auto
li
.articleItem
noSelect()
border solid 2px transparent
position relative
height 94px
width 100%
cursor pointer
transition 0.1s
.itemLeft
position absolute
top 4px
bottom 4px
width 38px
padding 3px 0 3px 3px
text-align center
.profileImage
margin-bottom 5px
circle()
.fa
line-height 25px
.itemRight
position absolute
top 4px
bottom 4px
right 2px
left 40px
overflow-x hidden
padding 3px 10px 3px 3px
.itemInfo
margin 5px 0 13px
color lighten(textColor, 25%)
font-size 0.7em
.userProfileName
color brandColor
font-size 1.2em
.description
line-height 120%
margin-bottom 10px
font-size 1em
overflow-x hidden
white-space nowrap
text-overflow ellipsis
.tags
position absolute
bottom 5px
font-size 0.9em
&:hover, &.hover
background-color hoverBackgroundColor
&:active, &.active
background-color white
&:active, &.active
border-color brandBorderColor
.divider
border-bottom solid 1px borderColor
.PlanetArticleDetail
absolute right bottom
top 55px
left navigationWidth + articleListWidth
.detailHeader
border solid 2px transparent
position relative
height 105px
width 100%
transition 0.1s
.itemLeft
position absolute
top 7px
bottom 4px
width 38px
padding 3px 0 3px 3px
text-align center
.profileImage
margin-bottom 5px
circle()
.fa
line-height 25px
.itemRight
position absolute
top 7px
bottom 4px
right 2px
left 40px
overflow-x hidden
padding 3px 10px 3px 3px
.itemInfo
margin 5px 0 13px
color lighten(textColor, 25%)
font-size 0.7em
.userProfileName
color brandColor
font-size 1.2em
.description
line-height 120%
margin-bottom 10px
font-size 1em
overflow-x auto
white-space nowrap
.tags
position absolute
bottom 5px
font-size 0.9em
.itemControl
position absolute
z-index 1
top 2px
right 2px
.deleteButton, .editButton
btnDefault()
text-align center
width 33px
height 33px
border-radius 16.5px
font-size 15px
margin 0 3px
.tooltip
tooltip()
margin-top 10px
&:hover .tooltip
opacity 1
.editButton .tooltip
margin-left -12px
.deleteButton .tooltip
margin-left -26px
.detailBody
absolute left right bottom
top 105px
.content
position absolute
top 5px
bottom 5px
left 2px
right 2px
box-sizing border-box
padding 5px
border-top solid 1px borderColor
&.noteDetail
.detailBody .content
overflow-x hidden
overflow-y auto
marked()
&.codeDetail
.detailBody .content
.ace_editor
absolute left right top bottom

View File

@@ -1,123 +0,0 @@
userNavigatorWidth = 200px
userNavigatorBgColor = #333
userNavigatorColor = #DDD
userNavigatorProfileNameColor = brandColor
userNavigatorBorderColor = #666
userContentBgColor = #E6E6E6
.UserContainer
absolute top bottom right
left 60px
.content
absolute top bottom right
left userNavigatorWidth
background-color userContentBgColor
.UserNavigator
absolute left top bottom
width userNavigatorWidth
background-color userNavigatorBgColor
color userNavigatorColor
noSelect()
&>.profile
height 60px
padding 10px 15px 0
box-sizing border-box
position relative
border-bottom solid 1px userNavigatorBorderColor
cursor pointer
&>.profileName
color userNavigatorProfileNameColor
font-size 22px
cursor pointer
transition 0.1s
&>.name
padding 5px 10px
font-size 14px
color userNavigatorColor
cursor pointer
transition 0.1s
&>.dropdownIcon
position absolute
top 20px
right 25px
float right
width 20px
height 20px
line-height 20px
font-size 8px
border solid 1px userNavigatorColor
border-radius 12.5px
text-align center
transition 0.1s
&:hover
&>.profileName
color lighten(brandColor, 10%)
&>.name
color white
&>.dropdownIcon
border-color white
&:active
&>.dropdownIcon
background-color brandColor
border-color brandColor
&>.control
padding 15px 15px
&>.newPostButton
background-color brandColor
color white
height 44px
width 100%
border none
border-radius 5px
font-size 16px
font-weight 600
transition 0.1s
&:hover
background-color lighten(brandColor, 10%)
&>.menu
absolute left right bottom
top 134px
padding 15px 0
overflow auto
&>.menuGruop
&>.label
border-bottom 1px solid userNavigatorBorderColor
padding 10px 15px
font-size 18px
margin-bottom 10px
&>.plusButton
float right
width 20px
height 20px
margin-top -2.5px
margin-right -5px
line-height 15px
font-size 8px
border solid 1px userNavigatorColor
border-radius 10px
background-color transparent
text-align center
color userNavigatorColor
&:hover
border-color white
color white
&:active
background-color brandColor
border-color brandColor
&>.folders
.folderButton
padding 10px 25px
width 100%
background-color transparent
border none
font-size 14px
color userNavigatorColor
transition 0.1s
text-align left
&:hover
background-color transparentify(white, 20%)
color white
&.active
background-color brandColor
color white

View File

@@ -3,13 +3,13 @@
@import '../mixins/*'
global-reset()
@import '../shared/*'
@import './components/*'
@import './containers/*'
@import './HomeContainer'
@import './ArticleNavigator'
@import './ArticleTopbar'
@import './ArticleList'
@import './ArticleDetail'
@import './modal/*'
*
-webkit-app-region no-drag
-webkit-user-select none
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
html, body
width 100%
@@ -17,13 +17,13 @@ html, body
overflow hidden
body
font-family "Lato"
font-family DEFAULT_FONTS
color textColor
font-size fontSize
font-weight 400
button, input, select
font-family "Lato"
button, input, select, textarea
font-family DEFAULT_FONTS
div, span, a, button, input, textarea
box-sizing border-box
@@ -31,7 +31,7 @@ div, span, a, button, input, textarea
a
color brandColor
&:hover
color darken(brandColor, 15%)
color lighten(brandColor, 5%)
&:visited
color brandColor
@@ -126,3 +126,28 @@ textarea.block-input
margin-left -107px
&:hover .tooltip
opacity 1
.OSSAnnounceModal
height 250
text-align center
.OSSAnnounceModal-title
font-size 32px
padding 45px 0
.OSSAnnounceModal-link
display block
font-size 20px
margin 25px 0 65px
.OSSAnnounceModal-closeBtn
display block
margin 0 auto
border none
border-radius 5px
width 150px
height 33px
background-color brandColor
color white
opacity 0.7
&:hover
opacity 1
background-color lighten(brandColor, 10%)

View File

@@ -1,6 +1,7 @@
.EditedAlert.modal
width 350px
.DeleteArticleModal.modal
width 350px !important
top 100px
user-select none
.title
font-size 24px
margin-bottom 15px
@@ -20,9 +21,13 @@
margin-left 5px
&:hover
background-color darken(white, 10%)
&.primary
border-color brandColor
background-color brandColor
&:focus
border-color focusBorderColor
&.danger
border-color #E9432A
background-color #E9432A
color white
&:hover
background-color lighten(brandColor, 10%)
background-color lighten(#E9432A, 15%)
&:focus
background-color lighten(#E9432A, 15%)

View File

@@ -60,7 +60,7 @@ iptFocusBorderColor = #369DCD
left 180px
overflow-y auto
&>.section
padding 10px
padding 10px 20px
border-bottom 1px solid borderColor
overflow-y auto
&:nth-last-child(1)
@@ -69,17 +69,29 @@ iptFocusBorderColor = #369DCD
font-size 18px
margin 10px 0 5px
color brandColor
&>.sectionInput
&>.sectionCheck
margin-bottom 5px
height 33px
label
width 150px
padding-left 15px
line-height 33px
.sectionCheck-warn
font-size 12px
margin-left 10px
border-left 2px solid brandColor
padding-left 5px
&>.sectionInput
margin-bottom 5px
clearfix()
height 33px
label
width 180px
width 150px
padding-left 15px
float left
line-height 33px
input
width 300px
width 250px
float left
height 33px
border-radius 5px
@@ -89,6 +101,51 @@ iptFocusBorderColor = #369DCD
outline none
&:focus
border-color iptFocusBorderColor
&>.sectionSelect
margin-bottom 5px
clearfix()
height 33px
label
width 150px
padding-left 15px
float left
line-height 33px
select
float left
width 200px
height 25px
margin-top 4px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
&:focus
border-color iptFocusBorderColor
&>.sectionMultiSelect
margin-bottom 5px
clearfix()
height 33px
label
width 150px
padding-left 15px
float left
line-height 33px
.sectionMultiSelect-input
float left
select
width 80px
height 25px
margin-top 4px
border-radius 5px
border 1px solid borderColor
padding 0 10px
font-size 14px
outline none
margin-left 5px
margin-right 15px
&:focus
border-color iptFocusBorderColor
&>.sectionConfirm
clearfix()
padding 5px 15px
@@ -120,69 +177,14 @@ iptFocusBorderColor = #369DCD
color errorTextColor
background-color errorBackgroundColor
&.ContactTab
&.done
.message
margin-top 75px
margin-bottom 15px
text-align center
font-size 22px
.checkIcon
margin-bottom 15px
font-size 144px
color brandColor
text-align center
.control
text-align center
button
border solid 1px borderColor
border-radius 5px
background-color white
padding 15px 15px
font-size 14px
&:hover
background-color darken(white, 10%)
&.form
padding 10px
.title
font-size 18px
color brandColor
margin-top 10px
margin-bottom 10px
.description
margin-bottom 15px
.iptGroup
margin-bottom 10px
input, textarea
border-radius 5px
border 1px solid borderColor
font-size 14px
outline none
padding 10px 15px
width 100%
&:focus
border-color iptFocusBorderColor
textarea
resize vertical
min-height 150px
.formControl
clearfix()
.alert
float right
padding 10px 15px
margin 0 5px 0
font-size 14px
line-height normal
button
padding 10px 15px
background-color brandColor
color white
font-size 14px
border-radius 5px
border none
float right
&:hover
background-color lighten(brandColor, 10%)
padding 10px
.title
font-size 18px
color brandColor
margin-top 10px
margin-bottom 10px
p
line-height 2
&.AppSettingTab
.description
marked()
@@ -624,10 +626,3 @@ iptFocusBorderColor = #369DCD
color brandColor
&:hover
color lighten(brandColor, 10%)

View File

@@ -7,7 +7,7 @@ slideBgColor4 = #00B493
.Tutorial.modal
background-color slideBgColor0
color white
color white !important
width 720px
height 480px
margin-top 75px

View File

@@ -1,83 +1,137 @@
marked()
h1, h2, h3, h4, h5, h6, p
&:first-child
margin-top 0
font-size 14px
div.math-rendered
text-align center
.math-failed
background-color alpha(red, 0.1)
color darken(red, 15%)
padding 5px
margin -5px
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 0 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
font-size 2em
border-bottom solid 2px borderColor
margin 0.33em auto 0.67em
h2
font-size 1.5em
margin 0.42em auto 0.83em
h3
font-size 1.17em
margin 0.5em auto 1em
h4
font-size 1em
margin 0.67em auto 1.33em
h5
font-size 0.83em
margin 0.84em auto 1.67em
h6
font-size 0.67em
margin 1.16em auto 2.33em
h1, h2, h3, h4, h5, h6
font-weight 700
margin 0 0 15px
font-weight 600
*:not(a.lineAnchor) + h1, *:not(a.lineAnchor) + h2, *:not(a.lineAnchor) + h3, *:not(a.lineAnchor) + h4, *:not(a.lineAnchor) + h5, *:not(a.lineAnchor) + h6
margin-top 25px
h1
font-size 1.8em
border-bottom solid 2px borderColor
line-height 2em
h2
font-size 1.66em
line-height 1.8em
h3
font-size 1.33em
line-height 1.6625em
h4
font-size 1.15em
line-height 1.4375em
h5
font-size 1em
line-height 1.25em
h6
font-size 0.8em
line-height 1em
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
margin-top 15px
p
line-height 1.8em
margin 15px 0 25px
line-height 1.9em
margin 0 0 15px
img
max-width 100%
strong
strong, b
font-weight bold
em
em, i
font-style italic
s
s, del, strike
text-decoration line-through
u
text-decoration underline
blockquote
border-left solid 4px brandBorderColor
margin 15px 0 25px
margin 0 0 15px
padding 0 25px
ul
list-style-type disc
padding-left 35px
margin-bottom 35px
padding-left 25px
margin-bottom 15px
li
display list-item
line-height 1.8em
&>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 35px
margin-bottom 35px
padding-left 25px
margin-bottom 15px
li
display list-item
line-height 1.8em
&>li>ul, &>li>ol
margin 0
code
font-family monospace
font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
padding 2px 4px
border solid 1px borderColor
border solid 1px alpha(borderColor, 0.3)
border-radius 4px
font-size 0.9em
color black
text-decoration none
background-color #F6F6F6
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre
padding 5px
border solid 1px borderColor
border solid 1px alpha(borderColor, 0.3)
border-radius 5px
overflow-x auto
margin 15px 0 25px
margin 0 0 15px
background-color #F6F6F6
line-height 1.35em
&>code
margin 0
padding 0
border none
border-radius 0

View File

@@ -3,6 +3,8 @@ highlightenBorderColor = darken(borderColor, 20%)
invBorderColor = #404849
brandBorderColor = #3FB399
focusBorderColor = #369DCD
buttonBorderColor = #4C4C4C
lightButtonColor = #898989

View File

@@ -1,3 +0,0 @@
// export const API_URL = 'http://localhost:8000/'
export const API_URL = 'http://boost-api4.elasticbeanstalk.com/'
// export API_URL 'https://api2.b00st.io/'

35
contributing.md Normal file
View File

@@ -0,0 +1,35 @@
# Contributing to Boostnote
> English below.
## About Pull Request
### やり方
現状特に`dev`ブランチを用意しないつもりなので、最新VersionのブランチにPullrequestを送ってください。
### Pull requsetの著作権
Pull requestをすることはその変化分のコードの著作権をMAISIN&CO.に譲渡することに同意することになります。
アプリケーションのLicenseのをいつでも変える選択肢を残したいからです。
しかし、これはいずれかBoostnoteが有料の商用アプリになる可能性がある話ではありません。
もし、このアプリケーションで金を稼ごうとするならBoostnote専用のCloud storageの提供やMobile appとの連動、何か特殊なプレミアム機能の提供など形になると思います。
現在考えられているのは、GPL v3の場合、他のライセンスとの互換が不可能であるため、もしより自由なLicense(BSD, MIT)に変える時に改めて著作権者としてライセンスし直す選択肢を残したいぐらいのイメージです。
---
# Contributing to Boostnote(ENG)
## About Pull Request
### How to
Make a new PR to the branch named latest version. This is because there is no `dev` branch currently.
### Copyright of Pull Request
If you make a pull request, It means you agree to transfer the copyright of the code changes to MAISIN&CO.
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.

212
gruntfile.js Normal file
View File

@@ -0,0 +1,212 @@
const path = require('path')
const ChildProcess = require('child_process')
const packager = require('electron-packager')
module.exports = function (grunt) {
var auth_code
try {
auth_code = grunt.file.readJSON('secret/auth_code.json')
} catch (e) {
if (e.origError.code === 'ENOENT') {
console.warn('secret/auth_code.json is not found. CodeSigning is not available.')
}
}
const OSX_COMMON_NAME = auth_code != null ? auth_code.OSX_COMMON_NAME : ''
const WIN_CERT_PASSWORD = auth_code != null ? auth_code.WIN_CERT_PASSWORD : ''
var initConfig = {
pkg: grunt.file.readJSON('package.json'),
'create-windows-installer': {
x64: {
appDirectory: path.join(__dirname, 'dist', 'Boostnote-win32-x64'),
outputDirectory: path.join(__dirname, 'dist'),
authors: 'MAISIN&CO., Inc.',
exe: 'Boostnote.exe',
loadingGif: path.join(__dirname, 'resources/boostnote-install.gif'),
iconUrl: path.join(__dirname, 'resources/app.ico'),
setupIcon: path.join(__dirname, 'resources/dmg.ico'),
certificateFile: path.join(__dirname, 'secret', 'authenticode_cer.p12'),
certificatePassword: WIN_CERT_PASSWORD,
noMsi: true
}
}
}
grunt.initConfig(initConfig)
grunt.loadNpmTasks('grunt-electron-installer')
grunt.registerTask('compile', function () {
var done = this.async()
var execPath = path.join('node_modules', '.bin', 'webpack') + ' --config webpack-production.config.js'
grunt.log.writeln(execPath)
ChildProcess.exec(execPath,
{
env: Object.assign({}, process.env, {
BABEL_ENV: 'production',
NODE_ENV: 'production'
})
},
function (err, stdout, stderr) {
grunt.log.writeln(stdout)
if (err) {
grunt.log.writeln(err)
grunt.log.writeln(stderr)
done(false)
return
}
done()
}
)
})
grunt.registerTask('pack', function (platform) {
grunt.log.writeln(path.join(__dirname, 'dist'))
var done = this.async()
var opts = {
name: 'Boostnote',
arch: 'x64',
dir: __dirname,
version: grunt.config.get('pkg.config.electron-version'),
'app-version': grunt.config.get('pkg.version'),
'app-bundle-id': 'com.maisin.boost',
asar: true,
prune: true,
overwrite: true,
out: path.join(__dirname, 'dist'),
ignore: /submodules\/ace\/(?!src-min)|submodules\/ace\/(?=src-min-noconflict)|node_modules\/devicon\/icons|dist|^\/browser|^\/secret|\.babelrc|\.gitignore|^\/\.gitmodules|^\/gruntfile|^\/readme.md|^\/webpack|^\/appdmg\.json/
}
switch (platform) {
case 'win':
Object.assign(opts, {
platform: 'win32',
icon: path.join(__dirname, 'resources/app.ico'),
'version-string': {
CompanyName: 'MAISIN&CO., Inc.',
LegalCopyright: '© 2015 MAISIN&CO., Inc. All rights reserved.',
FileDescription: 'Boostnote',
OriginalFilename: 'Boostnote',
FileVersion: grunt.config.get('pkg.version'),
ProductVersion: grunt.config.get('pkg.version'),
ProductName: 'Boostnote',
InternalName: 'Boostnote'
}
})
packager(opts, function (err, appPath) {
if (err) {
grunt.log.writeln(err)
done(err)
return
}
done()
})
break
case 'osx':
Object.assign(opts, {
platform: 'darwin',
icon: path.join(__dirname, 'resources/app.icns'),
'app-category-type': 'public.app-category.developer-tools'
})
packager(opts, function (err, appPath) {
if (err) {
grunt.log.writeln(err)
done(err)
return
}
done()
})
break
}
})
grunt.registerTask('codesign', function (platform) {
var done = this.async()
if (process.platform !== 'darwin') {
done(false)
return
}
ChildProcess.exec(`codesign --verbose --deep --force --sign \"${OSX_COMMON_NAME}\" dist/Boostnote-darwin-x64/Boostnote.app`,
function (err, stdout, stderr) {
grunt.log.writeln(stdout)
if (err) {
grunt.log.writeln(err)
grunt.log.writeln(stderr)
done(false)
return
}
done()
})
})
grunt.registerTask('create-osx-installer', function () {
var done = this.async()
var execPath = 'appdmg appdmg.json dist/Boostnote-mac.dmg'
grunt.log.writeln(execPath)
ChildProcess.exec(execPath,
function (err, stdout, stderr) {
grunt.log.writeln(stdout)
if (err) {
grunt.log.writeln(err)
grunt.log.writeln(stderr)
done(false)
return
}
done()
})
})
grunt.registerTask('zip', function (platform) {
var done = this.async()
switch (platform) {
case 'osx':
var execPath = 'cd dist/Boostnote-darwin-x64 && zip -r -y -q ../Boostnote-mac.zip Boostnote.app'
grunt.log.writeln(execPath)
ChildProcess.exec(execPath,
function (err, stdout, stderr) {
grunt.log.writeln(stdout)
if (err) {
grunt.log.writeln(err)
grunt.log.writeln(stderr)
done(false)
return
}
done()
}
)
break
default:
done()
return
}
})
grunt.registerTask('build', function (platform) {
if (!platform) {
platform = process.platform === 'darwin' ? 'osx' : process.platform === 'win32' ? 'win' : null
}
switch (platform) {
case 'win':
grunt.task.run(['compile', 'pack:win', 'create-windows-installer'])
break
case 'osx':
grunt.task.run(['compile', 'pack:osx', 'codesign', 'create-osx-installer', 'zip:osx'])
break
}
})
grunt.registerTask('pre-build', function (platform) {
if (!platform) {
platform = process.platform === 'darwin' ? 'osx' : process.platform === 'win32' ? 'win' : null
}
switch (platform) {
case 'win':
grunt.task.run(['compile', 'pack:win'])
break
case 'osx':
grunt.task.run(['compile', 'pack:osx'])
break
}
})
grunt.registerTask('default', ['build'])
}

10
index.js Normal file
View File

@@ -0,0 +1,10 @@
function isFinderCalled () {
var argv = process.argv.slice(1)
return argv.some(arg => arg.match(/--finder/))
}
if (isFinderCalled()) {
require('./lib/finder-app')
} else {
require('./lib/main-app')
}

View File

@@ -1,191 +0,0 @@
import superagent from 'superagent'
import superagentPromise from 'superagent-promise'
import auth from 'boost/auth'
export const API_URL = 'http://boost-api4.elasticbeanstalk.com/'
export const WEB_URL = 'https://b00st.io/'
// export const WEB_URL = 'http://localhost:3333/'
export const request = superagentPromise(superagent, Promise)
export function login (input) {
return request
.post(API_URL + 'auth/login')
.send(input)
}
export function signup (input) {
return request
.post(API_URL + 'auth/register')
.send(input)
}
export function updateUserInfo (input) {
return request
.put(API_URL + 'auth/user')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function updatePassword (input) {
return request
.post(API_URL + 'auth/password')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function fetchCurrentUser () {
return request
.get(API_URL + 'auth/user')
.set({
Authorization: 'Bearer ' + auth.token()
})
}
export function fetchArticles (userId) {
return request
.get(API_URL + 'teams/' + userId + '/articles')
.set({
Authorization: 'Bearer ' + auth.token()
})
}
export function createArticle (input) {
return request
.post(API_URL + 'articles/')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function saveArticle (input) {
return request
.put(API_URL + 'articles/' + input.id)
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function destroyArticle (articleId) {
return request
.del(API_URL + 'articles/' + articleId)
.set({
Authorization: 'Bearer ' + auth.token()
})
}
export function createTeam (input) {
return request
.post(API_URL + 'teams')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function updateTeamInfo (teamId, input) {
return request
.put(API_URL + 'teams/' + teamId)
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function destroyTeam (teamId) {
return request
.del(API_URL + 'teams/' + teamId)
.set({
Authorization: 'Bearer ' + auth.token()
})
}
export function searchUser (key) {
return request
.get(API_URL + 'search/users')
.query({key: key})
}
export function setMember (teamId, input) {
return request
.post(API_URL + 'teams/' + teamId + '/members')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function deleteMember (teamId, input) {
return request
.del(API_URL + 'teams/' + teamId + '/members')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function createFolder (input) {
return request
.post(API_URL + 'folders/')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function updateFolder (id, input) {
return request
.put(API_URL + 'folders/' + id)
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export function destroyFolder (id) {
return request
.del(API_URL + 'folders/' + id)
.set({
Authorization: 'Bearer ' + auth.token()
})
}
export function sendEmail (input) {
return request
.post(API_URL + 'mail')
.set({
Authorization: 'Bearer ' + auth.token()
})
.send(input)
}
export default {
API_URL,
WEB_URL,
request,
login,
signup,
updateUserInfo,
updatePassword,
fetchCurrentUser,
fetchArticles,
createArticle,
saveArticle,
destroyArticle,
createTeam,
updateTeamInfo,
destroyTeam,
searchUser,
setMember,
deleteMember,
createFolder,
updateFolder,
destroyFolder,
sendEmail
}

View File

@@ -1,34 +0,0 @@
// initial value
var currentUser = JSON.parse(localStorage.getItem('currentUser'))
var currentToken = localStorage.getItem('token')
function user (user, newToken) {
if (user != null) {
localStorage.setItem('currentUser', JSON.stringify(user))
currentUser = user
}
if (newToken != null) {
localStorage.setItem('token', newToken)
currentToken = newToken
}
return currentUser
}
function token () {
return currentToken
}
function clear () {
localStorage.removeItem('currentUser')
localStorage.removeItem('token')
currentUser = null
currentToken = null
}
export default {
user,
token,
clear
}

View File

@@ -1,75 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import modes from 'boost/vars/modes'
import _ from 'lodash'
var ace = window.ace
module.exports = React.createClass({
propTypes: {
code: React.PropTypes.string,
mode: React.PropTypes.string,
className: React.PropTypes.string,
onChange: React.PropTypes.func,
readOnly: React.PropTypes.bool
},
getDefaultProps: function () {
return {
readOnly: false
}
},
componentWillReceiveProps: function (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
}
},
componentDidMount: function () {
var el = ReactDOM.findDOMNode(this.refs.target)
var editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.setValue(this.props.code)
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/xcode')
editor.clearSelection()
editor.setReadOnly(!!this.props.readOnly)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(true)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.on('change', function (e) {
if (this.props.onChange != null) {
var value = editor.getValue()
this.props.onChange(e, value)
}
}.bind(this))
this.setState({editor: editor})
},
componentDidUpdate: function (prevProps) {
if (this.state.editor.getValue() !== this.props.code) {
this.state.editor.setValue(this.props.code)
this.state.editor.clearSelection()
}
if (prevProps.mode !== this.props.mode) {
var session = this.state.editor.getSession()
let mode = _.findWhere(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
}
},
render: function () {
return (
<div ref='target' className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}></div>
)
}
})

View File

@@ -1,55 +0,0 @@
import shell from 'shell'
var React = require('react')
var { PropTypes } = React
import markdown from 'boost/markdown'
var ReactDOM = require('react-dom')
function handleAnchorClick (e) {
shell.openExternal(e.target.href)
e.preventDefault()
}
export default class MarkdownPreview extends React.Component {
componentDidMount () {
this.addListener()
}
componentDidUpdate () {
this.addListener()
}
componentWillUnmount () {
this.removeListener()
}
componentWillUpdate () {
this.removeListener()
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].addEventListener('click', handleAnchorClick)
}
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a')
for (var i = 0; i < anchors.length; i++) {
anchors[i].removeEventListener('click', handleAnchorClick)
}
}
render () {
return (
<div className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '')} dangerouslySetInnerHTML={{__html: ' ' + markdown(this.props.content)}}/>
)
}
}
MarkdownPreview.propTypes = {
className: PropTypes.string,
content: PropTypes.string
}

View File

@@ -1,85 +0,0 @@
import React, { PropTypes, findDOMNode } from 'react'
import linkState from 'boost/linkState'
import { sendEmail } from 'boost/api'
export default class ContactModal extends React.Component {
constructor (props) {
super(props)
this.linkState = linkState
this.state = {
isSent: false,
mail: {
title: '',
content: ''
}
}
}
onKeyCast (e) {
switch (e.status) {
case 'closeModal':
this.props.close()
break
case 'submitContactModal':
if (this.state.isSent) {
this.props.close()
return
}
this.sendEmail()
break
}
}
componentDidMount () {
findDOMNode(this.refs.title).focus()
}
sendEmail () {
sendEmail(this.state.mail)
.then(function (res) {
this.setState({isSent: !this.state.isSent})
}.bind(this))
.catch(function (err) {
console.error(err)
})
}
render () {
return (
<div className='ContactModal modal'>
<div className='modal-header'><h1>Contact form</h1></div>
{!this.state.isSent ? (
<div className='contactForm'>
<div className='modal-body'>
<div className='formField'>
<input ref='title' valueLink={this.linkState('mail.title')} placeholder='Title'/>
</div>
<div className='formField'>
<textarea valueLink={this.linkState('mail.content')} placeholder='Content'/>
</div>
</div>
<div className='modal-footer'>
<div className='formControl'>
<button onClick={this.sendEmail} className='sendButton'>Send</button>
<button onClick={this.props.close}>Cancel</button>
</div>
</div>
</div>
) : (
<div className='confirmation'>
<div className='confirmationMessage'>Thanks for sharing your opinion!</div>
<button className='doneButton' onClick={this.props.close}>Done</button>
</div>
)}
</div>
)
}
}
ContactModal.propTypes = {
close: PropTypes.func
}

View File

@@ -1,255 +0,0 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import { searchUser, createTeam, setMember, deleteMember } from 'boost/api'
import linkState from 'boost/linkState'
import Select from 'react-select'
function getUsers (input, cb) {
searchUser(input)
.then(function (res) {
let users = res.body
cb(null, {
options: users.map(user => {
return { value: user.name, label: user.name }
}),
complete: false
})
})
.catch(function (err) {
console.error(err)
})
}
export default class CreateNewTeam extends React.Component {
constructor (props) {
super(props)
this.state = {
create: {
name: '',
alert: null
},
select: {
team: null,
newMember: null,
alert: null
},
currentTab: 'create',
currentUser: JSON.parse(localStorage.getItem('currentUser'))
}
}
handleCloseClick (e) {
this.props.close()
}
handleContinueClick (e) {
let createState = this.state.create
createState.isSending = true
createState.alert = {
type: 'info',
message: 'sending...'
}
this.setState({create: createState})
function onTeamCreate (res) {
let createState = this.state.create
createState.isSending = false
createState.alert = null
let selectState = this.state.select
selectState.team = res.body
this.setState({
currentTab: 'select',
create: createState,
select: {
team: res.body
}
})
}
function onError (err) {
let errorMessage = err.response != null ? err.response.body.message : 'Can\'t connect to API server.'
let createState = this.state.create
createState.isSending = false
createState.alert = {
type: 'error',
message: errorMessage
}
this.setState({
create: createState
})
}
createTeam({name: this.state.create.name})
.then(onTeamCreate.bind(this))
.catch(onError.bind(this))
}
renderCreateTab () {
let createState = this.state.create
let alertEl = createState.alert != null ? (
<p className={['alert'].concat([createState.alert.type]).join(' ')}>{createState.alert.message}</p>
) : null
return (
<div className='createTab'>
<div className='title'>Create new team</div>
<input valueLink={this.linkState('create.name')} className='ipt' type='text' placeholder='Enter your team name'/>
{alertEl}
<button onClick={e => this.handleContinueClick(e)} disabled={createState.isSending} className='confirmBtn'>Continue <i className='fa fa-arrow-right fa-fw'/></button>
</div>
)
}
handleNewMemberChange (value) {
let selectState = this.state.select
selectState.newMember = value
this.setState({select: selectState})
}
handleClickAddMemberButton (e) {
let selectState = this.state.select
let input = {
name: selectState.newMember,
role: 'member'
}
setMember(selectState.team.id, input)
.then(res => {
let selectState = this.state.select
let team = res.body
team.Members = team.Members.sort((a, b) => {
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
})
selectState.team = team
selectState.newMember = ''
this.setState({select: selectState})
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
handleMemberDeleteButtonClick (name) {
let selectState = this.state.select
let input = {
name: name
}
return e => {
deleteMember(selectState.team.id, input)
.then(res => {
let selectState = this.state.select
let team = res.body
team.Members = team.Members.sort((a, b) => {
return new Date(a._pivot_createdAt) - new Date(b._pivot_createdAt)
})
selectState.team = team
selectState.newMember = ''
this.setState({select: selectState})
})
.catch(err => {
console.log(err, err.response)
if (err.status != null) throw err
else console.error(err)
})
}
}
handleMemberRoleChange (name) {
return function (e) {
let selectState = this.state.select
let input = {
name: name,
role: e.target.value
}
setMember(selectState.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}.bind(this)
}
renderSelectTab () {
let selectState = this.state.select
let membersEl = selectState.team.Members.map(member => {
let isCurrentUser = this.state.currentUser.id === member.id
return (
<li key={'user-' + member.id}>
<ProfileImage className='userPhoto' email={member.email} size='30'/>
<div className='userInfo'>
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
<div className='userEmail'>{member.email}</div>
</div>
<div className='userControl'>
<select onChange={e => this.handleMemberRoleChange(member.name)(e)} disabled={isCurrentUser} value={member._pivot_role} className='userRole'>
<option value='owner'>Owner</option>
<option value='member'>Member</option>
</select>
<button onClick={e => this.handleMemberDeleteButtonClick(member.name)(e)} disabled={isCurrentUser}><i className='fa fa-times fa-fw'/></button>
</div>
</li>
)
})
return (
<div className='selectTab'>
<div className='title'>Select member</div>
<div className='memberForm'>
<Select
className='memberName'
autoload={false}
asyncOptions={getUsers}
onChange={val => this.handleNewMemberChange(val)}
value={selectState.newMember}
/>
<button onClick={e => this.handleClickAddMemberButton(e)} className='addMemberBtn'>add</button>
</div>
<ul className='memberList'>
{membersEl}
</ul>
<button onClick={e => this.props.close(e)}className='confirmBtn'>Done</button>
</div>
)
}
render () {
let currentTab = this.state.currentTab === 'create'
? this.renderCreateTab()
: this.renderSelectTab()
return (
<div className='CreateNewTeam modal'>
<button onClick={e => this.handleCloseClick(e)} className='closeBtn'><i className='fa fa-fw fa-times'/></button>
{currentTab}
<div className='tabNav'>
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'create' ? ' active' : '')}/>
<i className={'fa fa-circle fa-fw' + (this.state.currentTab === 'select' ? ' active' : '')}/>
</div>
</div>
)
}
}
CreateNewTeam.propTypes = {
close: PropTypes.func
}
CreateNewTeam.prototype.linkState = linkState

View File

@@ -1,35 +0,0 @@
import React, { PropTypes } from 'react'
import store from 'boost/store'
import { unlockStatus } from 'boost/actions'
export default class EditedAlert extends React.Component {
handleNoButtonClick (e) {
this.props.close()
}
handleYesButtonClick (e) {
store.dispatch(unlockStatus())
store.dispatch(this.props.action)
this.props.close()
}
render () {
return (
<div className='EditedAlert modal'>
<div className='title'>Your article is still editing!</div>
<div className='message'>Do you really want to leave without finishing?</div>
<div className='control'>
<button onClick={e => this.handleNoButtonClick(e)}><i className='fa fa-fw fa-close'/> No</button>
<button onClick={e => this.handleYesButtonClick(e)} className='primary'><i className='fa fa-fw fa-check'/> Yes</button>
</div>
</div>
)
}
}
EditedAlert.propTypes = {
action: PropTypes.object,
close: PropTypes.func
}

View File

@@ -1,99 +0,0 @@
import React from 'react'
import linkState from 'boost/linkState'
import remote from 'remote'
import ipc from 'ipc'
export default class AppSettingTab extends React.Component {
constructor (props) {
super(props)
let keymap = remote.getGlobal('keymap')
this.state = {
toggleFinder: keymap.toggleFinder,
alert: null
}
}
componentDidMount () {
this.handleSettingDone = () => {
this.setState({alert: {
type: 'success',
message: 'Successfully done!'
}})
}
this.handleSettingError = err => {
this.setState({alert: {
type: 'error',
message: err.message
}})
}
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)
}
submitHotKey () {
ipc.send('hotkeyUpdated', {
toggleFinder: this.state.toggleFinder
})
}
handleSaveButtonClick (e) {
this.submitHotKey()
}
handleKeyDown (e) {
this.submitHotKey()
}
render () {
let alert = this.state.alert
let alertElement = alert != null ? (
<p className={`alert ${alert.type}`}>
{alert.message}
</p>
) : null
return (
<div className='AppSettingTab content'>
<div className='section'>
<div className='sectionTitle'>Hotkey</div>
<div className='sectionInput'>
<label>Toggle Finder(popup)</label>
<input onKeyDown={e => this.handleKeyDown(e)} valueLink={this.linkState('toggleFinder')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{alertElement}
</div>
<div className='description'>
<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>
)
}
}
AppSettingTab.prototype.linkState = linkState

View File

@@ -1,123 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { getClientKey } from 'boost/activityRecord'
import linkState from 'boost/linkState'
import _ from 'lodash'
import { request, WEB_URL } from 'boost/api'
const FORM_MODE = 'FORM_MODE'
const DONE_MODE = 'DONE_MODE'
export default class ContactTab extends React.Component {
constructor (props) {
super(props)
this.state = {
title: '',
content: '',
email: '',
mode: FORM_MODE,
alert: null
}
}
componentDidMount () {
let titleInput = ReactDOM.findDOMNode(this.refs.title)
if (titleInput != null) titleInput.focus()
}
handleBackButtonClick (e) {
this.setState({
mode: FORM_MODE
})
}
handleSendButtonClick (e) {
let input = _.pick(this.state, ['title', 'content', 'email'])
input.clientKey = getClientKey()
this.setState({
alert: {
type: 'info',
message: 'Sending...'
}
}, () => {
request.post(WEB_URL + 'apis/inquiry')
.send(input)
.then(res => {
console.log('sent')
this.setState({
title: '',
content: '',
mode: DONE_MODE,
alert: null
})
})
.catch(err => {
if (err.code === 'ECONNREFUSED') {
this.setState({
alert: {
type: 'error',
message: 'Can\'t connect to API server.'
}
})
} else {
console.error(err)
this.setState({
alert: {
type: 'error',
message: err.message
}
})
}
})
})
}
render () {
switch (this.state.mode) {
case DONE_MODE:
return (
<div className='ContactTab content done'>
<div className='message'>
<i className='checkIcon fa fa-check-circle'/><br/>
Your message has been sent successfully!!
</div>
<div className='control'>
<button onClick={e => this.handleBackButtonClick(e)}>Back to Contact form</button>
</div>
</div>
)
case FORM_MODE:
default:
let alertElement = this.state.alert != null
? (
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
)
: null
return (
<div className='ContactTab content form'>
<div className='title'>Contact form</div>
<div className='description'>
Your feedback is highly appreciated and will help us to improve our app. :D
</div>
<div className='iptGroup'>
<input ref='title' valueLink={this.linkState('title')} placeholder='Title' type='text'/>
</div>
<div className='iptGroup'>
<textarea valueLink={this.linkState('content')} placeholder='Content'/>
</div>
<div className='iptGroup'>
<input valueLink={this.linkState('email')} placeholder='E-mail (Optional)' type='email'/>
</div>
<div className='formControl'>
<button onClick={e => this.handleSendButtonClick(e)} className='primary'>Send</button>
{alertElement}
</div>
</div>
)
}
}
}
ContactTab.prototype.linkState = linkState

View File

@@ -1,11 +0,0 @@
import React, { PropTypes } from 'react'
export default class HelpTab extends React.Component {
render () {
return (
<div className='content help'>
Comming soon
</div>
)
}
}

View File

@@ -1,106 +0,0 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import api from 'boost/api'
const IDLE = 'IDLE'
const DELETE = 'DELETE'
export default class MemberRow extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE
}
}
handleMemberRoleChange (e) {
let input = {
name: this.props.member.name,
role: e.target.value
}
api.setMember(this.props.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
handleDeleteButtonClick (e) {
this.setState({mode: DELETE})
}
handleCancelButtonClick (e) {
this.setState({mode: IDLE})
}
handleDeleteConfirmButtonClick (e) {
let input = {
name: this.props.member.name
}
api.deleteMember(this.props.team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
render () {
let member = this.props.member
let currentUser = this.props.currentUser
let isDisabled = (currentUser.id === member.id)
switch (this.state.mode) {
case DELETE:
return (
<li className='MemberRow edit'>
<div className='colDescription'>
Are you sure to remove <strong>{member.profileName}</strong> ?
</div>
<div className='colDeleteConfirm'>
<button className='deleteButton primary' onClick={e => this.handleDeleteConfirmButtonClick(e)}>Sure</button>
<button className='deleteButton' onClick={e => this.handleCancelButtonClick(e)}>Cancel</button>
</div>
</li>
)
case IDLE:
default:
return (
<li className='MemberRow'>
<div className='colUserName'>
<ProfileImage className='userPhoto' email={member.email} size='30'/>
<div className='userInfo'>
<div className='userName'>{`${member.profileName} (${member.name})`}</div>
<div className='userEmail'>{member.email}</div>
</div>
</div>
<div className='colRole'>
<select onChange={e => this.handleMemberRoleChange(e)} disabled={isDisabled} value={member._pivot_role} className='userRole'>
<option value='owner'>Owner</option>
<option value='member'>Member</option>
</select>
</div>
<div className='colDelete'>
<button className='deleteButton' onClick={e => this.handleDeleteButtonClick(e)} disabled={isDisabled}><i className='fa fa-times fa-fw'/></button>
</div>
</li>
)
}
}
}
MemberRow.propTypes = {
member: PropTypes.shape(),
currentUser: PropTypes.shape(),
team: PropTypes.shape({
id: PropTypes.number
})
}

View File

@@ -1,149 +0,0 @@
import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import Select from 'react-select'
import api from 'boost/api'
import _ from 'lodash'
import MemberRow from './MemberRow'
function getUsers (input, cb) {
api.searchUser(input)
.then(function (res) {
let users = res.body
cb(null, {
options: users.map(user => {
return { value: user.name, label: user.name }
}),
complete: false
})
})
.catch(function (err) {
console.error(err)
})
}
export default class MemberSettingTab extends React.Component {
constructor (props) {
super(props)
this.state = {
newMember: ''
}
}
getCurrentTeam (props) {
if (props == null) props = this.props
return _.findWhere(props.teams, {id: props.currentTeamId})
}
handleTeamSelectChange (e) {
this.props.switchTeam(e.target.value)
}
handleNewMemberChange (value) {
this.setState({newMember: value})
}
handleClickAddMemberButton (e) {
let team = this.getCurrentTeam()
if (team == null || team.userType !== 'team') return null
let input = {
name: this.state.newMember,
role: 'member'
}
api.setMember(team.id, input)
.then(res => {
console.log(res.body)
})
.catch(err => {
if (err.status != null) throw err
else console.error(err)
})
}
renderTeamOptions () {
return this.props.teams.map(team => {
return (
<option key={'team-' + team.id} value={team.id}>{team.name}</option>)
})
}
render () {
console.log(this.props.teams)
let team = this.getCurrentTeam()
if (team == null || team.userType === 'person') {
return this.renderNoTeam()
}
let membersEl = team.Members.map(member => (
<MemberRow key={'user-' + member.id} member={member} team={team} currentUser={this.props.currentUser}/>
))
return (
<div className='MemberSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='membersTableSection section'>
<div className='sectionTitle'>Members</div>
<div className='addMember'>
<div className='addMemberLabel'>Add member</div>
<div className='addMemberControl'>
<Select
className='memberName'
placeholder='Input username to add'
autoload={false}
asyncOptions={getUsers}
onChange={val => this.handleNewMemberChange(val)}
value={this.state.newMember}
/>
<button onClick={e => this.handleClickAddMemberButton(e)} className='addMemberBtn'>add</button>
</div>
</div>
<ul className='memberList'>
<li className='header'>
<div className='colUserName'>Username</div>
<div className='colRole'>Role</div>
<div className='colDelete'>Delete</div>
</li>
{membersEl}
</ul>
</div>
</div>
)
}
renderNoTeam () {
return (
<div className='TeamSettingTab content'>
<div className='header'>
<span>Setting of</span>
<select
value={this.props.currentTeamId}
onChange={e => this.handleTeamSelectChange(e)}
className='teamSelect'>
{this.renderTeamOptions()}
</select>
</div>
<div className='section'>Please select a team</div>
</div>
)
}
}
MemberSettingTab.propTypes = {
currentUser: PropTypes.shape(),
teams: PropTypes.array,
currentTeamId: PropTypes.number,
switchTeam: PropTypes.func
}

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