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

Compare commits

...

237 Commits

Author SHA1 Message Date
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
Rokt33r
e731b7882d Merge branch 'dev'
* dev:
  no source map
  bump version
  hidden code
2015-11-21 06:05:17 +09:00
Rokt33r
84e0728ff3 no source map 2015-11-21 06:03:32 +09:00
Rokt33r
666bc18e91 bump version 2015-11-21 05:56:29 +09:00
Rokt33r
8f83124a0d hidden code 2015-11-21 05:54:15 +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
218 changed files with 5469 additions and 5147 deletions

View File

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

6
.gitignore vendored
View File

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

2
LICENSE Normal file
View File

@@ -0,0 +1,2 @@
本製品をインストール、または使用することによって、お客様は利用規約(
https://b00st.io/regulations.htmlより拘束されることに承諾されたものとします。利用規約に同意されない場合、Boostnoteは、お客様に本製品のインストール、使用のいずれも許諾できません。

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,18 +6,32 @@ import { createStore } from 'redux'
import FinderInput from './FinderInput' import FinderInput from './FinderInput'
import FinderList from './FinderList' import FinderList from './FinderList'
import FinderDetail from './FinderDetail' import FinderDetail from './FinderDetail'
import { selectArticle, searchArticle, refreshData } from './actions' import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash' import _ from 'lodash'
import activityRecord from 'boost/activityRecord' import dataStore from 'browser/lib/dataStore'
import remote from 'remote' const electron = require('electron')
var hideFinder = remote.getGlobal('hideFinder') const { clipboard, ipcRenderer, remote } = electron
import clipboard from 'clipboard' const path = require('path')
var notifier = require('node-notifier') if (process.env.NODE_ENV !== 'production') {
var path = require('path') window.addEventListener('keydown', function (e) {
function getIconPath () { if (e.keyCode === 73 && e.metaKey && e.altKey) {
return path.resolve(global.__dirname, '../../resources/favicon-230x230.png') 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')
options.silent = false
}
return new window.Notification(title, options)
} }
require('../styles/finder/index.styl') require('../styles/finder/index.styl')
@@ -33,11 +47,21 @@ class FinderMain extends React.Component {
} }
componentDidMount () { componentDidMount () {
this.keyDownHandler = e => this.handleKeyDown(e)
document.addEventListener('keydown', this.keyDownHandler)
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
this.focusHandler = e => {
let { dispatch } = this.props
dispatch(searchArticle(''))
dispatch(selectArticle(null))
}
window.addEventListener('focus', this.focusHandler)
} }
handleClick (e) { componentWillUnmount () {
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus() document.removeEventListener('keydown', this.keyDownHandler)
window.removeEventListener('focus', this.focusHandler)
} }
handleKeyDown (e) { handleKeyDown (e) {
@@ -59,17 +83,20 @@ class FinderMain extends React.Component {
hideFinder() hideFinder()
e.preventDefault() e.preventDefault()
} }
if (e.keyCode === 91 || e.metaKey) {
return
}
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
} }
saveToClipboard () { saveToClipboard () {
let { activeArticle } = this.props let { activeArticle } = this.props
clipboard.writeText(activeArticle.content) clipboard.writeText(activeArticle.content)
activityRecord.emit('FINDER_COPY')
notifier.notify({ ipcRenderer.send('copy-finder')
icon: getIconPath(), notify('Saved to Clipboard!', {
'title': 'Saved to Clipboard!', body: 'Paste it wherever you want!'
'message': 'Paste it wherever you want!'
}) })
hideFinder() hideFinder()
} }
@@ -102,7 +129,7 @@ class FinderMain extends React.Component {
let { articles, activeArticle, status, dispatch } = this.props let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard() let saveToClipboard = () => this.saveToClipboard()
return ( return (
<div onClick={e => this.handleClick(e)} onKeyDown={e => this.handleKeyDown(e)} className='Finder'> <div className='Finder'>
<FinderInput <FinderInput
handleSearchChange={e => this.handleSearchChange(e)} handleSearchChange={e => this.handleSearchChange(e)}
ref='finderInput' ref='finderInput'
@@ -156,6 +183,14 @@ function buildFilter (key) {
return {type: TEXT_FILTER, value: key} return {type: TEXT_FILTER, value: key}
} }
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
}
function remap (state) { function remap (state) {
let { articles, folders, status } = state let { articles, folders, status } = state
@@ -172,10 +207,10 @@ function remap (state) {
let targetFolders let targetFolders
if (folders != null) { if (folders != null) {
let exactTargetFolders = folders.filter(folder => { let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`))) return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
}) })
let fuzzyTargetFolders = folders.filter(folder => { let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`))) return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
}) })
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders) targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
@@ -188,7 +223,7 @@ function remap (state) {
if (textFilters.length > 0) { if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => { articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => { return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i')) return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
}) })
}, articles) }, articles)
} }
@@ -196,7 +231,7 @@ function remap (state) {
if (tagFilters.length > 0) { if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => { articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => { return articles.filter(article => {
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i'))) return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
}) })
}, articles) }, articles)
} }
@@ -205,7 +240,6 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey}) let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0] if (activeArticle == null) activeArticle = articles[0]
console.log(status.search)
return { return {
articles, articles,
activeArticle, activeArticle,
@@ -216,13 +250,19 @@ function remap (state) {
var Finder = connect(remap)(FinderMain) var Finder = connect(remap)(FinderMain)
var store = createStore(reducer) var store = createStore(reducer)
function refreshData () {
let data = dataStore.getData(true)
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => { window.onfocus = e => {
store.dispatch(refreshData()) refreshData()
activityRecord.emit('FINDER_OPEN')
} }
ReactDOM.render(( ReactDOM.render((
<Provider store={store}> <Provider store={store}>
<Finder/> <Finder/>
</Provider> </Provider>
), document.getElementById('content')) ), document.getElementById('content'), function () {
refreshData()
})

View File

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

View File

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

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

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

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

@@ -0,0 +1,23 @@
import _ from 'lodash'
import keygen from './keygen'
function getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen()
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 () { module.exports = function () {
var shasum = crypto.createHash('sha1') 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') 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 React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
const remote = require('electron').remote
class ModalBase extends React.Component { class ModalBase extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
@@ -13,6 +15,8 @@ class ModalBase extends React.Component {
close () { close () {
if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true}) if (modalBase != null) modalBase.setState({component: null, componentProps: null, isHidden: true})
remote.getCurrentWebContents().send('list-focus')
} }
render () { render () {
@@ -38,9 +42,15 @@ export function openModal (component, props) {
export function closeModal () { export function closeModal () {
if (modalBase == null) { return } if (modalBase == null) { return }
modalBase.setState({component: null, componentProps: null, isHidden: true}) modalBase.close()
} }
export function isModalOpen () { export function isModalOpen () {
return !modalBase.state.isHidden return !modalBase.state.isHidden
} }
export default {
open: openModal,
close: closeModal,
isOpen: isModalOpen
}

View File

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

View File

@@ -1,4 +1,5 @@
var shell = require('shell') const electron = require('electron')
const shell = electron.shell
export default function (e) { export default function (e) {
shell.openExternal(e.currentTarget.href) 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,343 @@
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
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.state = {
article: Object.assign({content: ''}, props.activeArticle),
openShareDropdown: false
}
}
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)
}
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) {
}
}
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)}
/>
</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 React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ModeIcon from 'boost/components/ModeIcon' import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment' import moment from 'moment'
import { switchArticle, NEW } from 'boost/actions' import { switchArticle } from '../actions'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'browser/components/FolderMark'
import TagLink from 'boost/components/TagLink' import TagLink from './TagLink'
import _ from 'lodash' import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
export default class ArticleList extends React.Component { export default class ArticleList extends React.Component {
constructor (props) {
super(props)
this.focusHandler = e => this.focus()
}
componentDidMount () { componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000) this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ipc.on('list-focus', this.focusHandler)
this.focus()
} }
componentWillUnmount () { componentWillUnmount () {
clearInterval(this.refreshTimer) clearInterval(this.refreshTimer)
ipc.removeListener('list-focus', this.focusHandler)
} }
componentDidUpdate () { componentDidUpdate () {
@@ -36,6 +49,10 @@ export default class ArticleList extends React.Component {
} }
} }
focus () {
ReactDOM.findDOMNode(this).focus()
}
// 移動ができなかったらfalseを返す: // 移動ができなかったらfalseを返す:
selectPriorArticle () { selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props let { articles, activeArticle, dispatch } = this.props
@@ -64,36 +81,107 @@ export default class ArticleList extends React.Component {
handleArticleClick (article) { handleArticleClick (article) {
let { dispatch } = this.props let { dispatch } = this.props
return function (e) { return function (e) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key)) 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 () { render () {
let { articles, activeArticle, folders } = this.props let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => { 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 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}/>) return (<TagLink key={tag} tag={tag}/>)
}) })
: (<span>Not tagged yet</span>) : (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey}) 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 ( return (
<div key={'article-' + article.key}> <div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}> <div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'> <div className='ArticleList-item-top'>
{folder != null {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><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>
<div className='middle'> <div className='ArticleList-item-middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{article.status !== NEW ? article.title : '(New article)'}</div> <ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div> </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 className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div> </div>
</div> </div>
@@ -103,7 +191,7 @@ export default class ArticleList extends React.Component {
}) })
return ( return (
<div className='ArticleList'> <div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
{articleElements} {articleElements}
</div> </div>
) )
@@ -111,8 +199,9 @@ export default class ArticleList extends React.Component {
} }
ArticleList.propTypes = { ArticleList.propTypes = {
dispatch: PropTypes.func,
folders: PropTypes.array, folders: PropTypes.array,
articles: PropTypes.array, articles: PropTypes.array,
activeArticle: PropTypes.shape(), modified: PropTypes.array,
dispatch: PropTypes.func activeArticle: PropTypes.shape()
} }

View File

@@ -1,15 +1,17 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { findWhere } from 'lodash' import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, CREATE_MODE } from 'boost/actions' import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
import { openModal } from 'boost/modal' import { openModal, isModalOpen } from 'browser/lib/modal'
import FolderMark from 'boost/components/FolderMark' import FolderMark from 'browser/components/FolderMark'
import Preferences from 'boost/components/modal/Preferences' import Preferences from '../modal/Preferences'
import CreateNewFolder from 'boost/components/modal/CreateNewFolder' import CreateNewFolder from '../modal/CreateNewFolder'
import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
import remote from 'remote' const ipc = require('electron').ipcRenderer
let userName = remote.getGlobal('process').env.USER
const BRAND_COLOR = '#18AF90' const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const preferenceTutorialElement = ( const preferenceTutorialElement = (
<svg width='300' height='300' className='tutorial'> <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> </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 = ( const newFolderTutorialElement = (
<svg width='800' height='500' className='tutorial'> <svg width='800' height='500' className='tutorial'>
<text x='145' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text> <text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<svg x='115' y='-10' width='300' height='400'> <text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
<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 <svg x='50' y='10' width='300' height='400'>
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 <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
L36.6,3.7z'/> 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='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 <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
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 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
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 C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
c-6.8-7.3-25.8-19.1-32.3-4.8c-1.9,4.1,0.3,8.5,4.8,9.4c4.6,0.8,11.6-1.8,14.3-5.7c3.6-5.3-0.1-12.8-2.8-17.6
c-3.4-6.1-8.2-11.3-13.8-15.4C50.2,11.6,31,10.9,15.3,19C13.6,19.8,15.1,22.4,16.8,21.5L16.8,21.5z'/>
</svg> </svg>
</svg> </svg>
) )
export default class ArticleNavigator extends React.Component { 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) { handlePreferencesButtonClick (e) {
openModal(Preferences) openModal(Preferences)
} }
handleNewPostButtonClick (e) {
let { dispatch } = this.props
dispatch(switchMode(CREATE_MODE))
}
handleNewFolderButton (e) { handleNewFolderButton (e) {
let { activeUser } = this.props let { user } = this.props
openModal(CreateNewFolder, {user: activeUser}) openModal(CreateNewFolder, {user: user})
} }
handleFolderButtonClick (name) { handleFolderButtonClick (name) {
@@ -87,14 +80,58 @@ export default class ArticleNavigator extends React.Component {
dispatch(setSearchFilter('')) 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 () { render () {
let { status, folders, allArticles } = this.props let { status, user, folders, allArticles, modified, activeArticle } = this.props
let { targetFolders } = status let { targetFolders } = status
if (targetFolders == null) targetFolders = [] 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 folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key}) 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 ( return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}> <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 ( return (
<div className='ArticleNavigator'> <div tabIndex='1' className='ArticleNavigator'>
<div className='userInfo'> <div className='userInfo'>
<div className='userProfileName'>{userName}</div> <div className='userProfileName'>{user.name}</div>
<div className='userName'>localStorage</div> <div className='userName'>localStorage</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'> <button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
<i className='fa fa-fw fa-chevron-down'/> <i className='fa fa-fw fa-chevron-down'/>
@@ -117,26 +154,26 @@ export default class ArticleNavigator extends React.Component {
</div> </div>
<div className='controlSection'> {/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
<button onClick={e => this.handleNewPostButtonClick(e)} className='newPostBtn'> <div className='ArticleNavigator-unsaved-header'>Work in progress</div>
New Post <div className='ArticleNavigator-unsaved-list'>
<span className='tooltip'>Create a new Post ( + Enter or a)</span> {modifiedElements}
</button> </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={'ArticleNavigator-folders expand'}>
<div className='header'> {status.isTutorialOpen ? newFolderTutorialElement : null}
<div className='ArticleNavigator-folders-header'>
<div className='title'>Folders</div> <div className='title'>Folders</div>
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'> <button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
<i className='fa fa-fw fa-plus'/> <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> </button>
{status.isTutorialOpen ? newFolderTutorialElement : null}
</div> </div>
<div className='folderList'> <div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button> <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 = { ArticleNavigator.propTypes = {
activeUser: PropTypes.object, dispatch: PropTypes.func,
folders: PropTypes.array,
allArticles: PropTypes.array,
status: PropTypes.shape({ status: PropTypes.shape({
folderId: PropTypes.number 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 React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ExternalLink from 'boost/components/ExternalLink' import ExternalLink from 'browser/components/ExternalLink'
import { setSearchFilter, clearSearch, toggleTutorial } from 'boost/actions' 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 BRAND_COLOR = '#18AF90'
const searchTutorialElement = ( const searchTutorialElement = (
<svg width='750' height='120' className='tutorial'> <svg width='750' height='300' className='tutorial'>
<text x='450' y='33' fill={BRAND_COLOR} fontSize='24'>Search some posts!!</text> <text x='125' y='63' 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='125' y='90' fill={BRAND_COLOR} fontSize='18'>{'- Search by tag : #{string}'}</text>
<text x='450' y='85' fill={BRAND_COLOR} fontSize='18'> <text x='125' y='115' fill={BRAND_COLOR} fontSize='18'>
{'- Search by folder : /{folder_name}\n'}</text> {'- 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> {'exact match : //{folder_name}'}</text>
<svg width='500' height='300'> <svg x='90' 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 <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
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 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
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 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
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 c2-1.5,3.6-2.6,4.7-4.9C30.6,6.7,28,5.2,27.2,6.9L27.2,6.9z'/>
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 <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
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 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
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 C6.1,23.7,8.2,25.9,9.5,24.4L9.5,24.4z'/>
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 </svg>
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'/> </svg>
<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'/>
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>
</svg> </svg>
) )
@@ -34,19 +54,55 @@ export default class ArticleTopBar extends React.Component {
constructor (props) { constructor (props) {
super(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 = { this.state = {
isTooltipHidden: true isTooltipHidden: true,
isLinksDropdownOpen: false
} }
} }
componentDidMount () { componentDidMount () {
this.searchInput = ReactDOM.findDOMNode(this.refs.searchInput) 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 () { componentWillUnmount () {
this.searchInput.removeEventListener('keydown', this.showTooltip) document.removeEventListener('click', this.hideLinksDropdown)
this.searchInput.removeEventListener('focus', this.showTooltip) this.linksButton.removeEventListener('click', this.showLinksDropdown())
this.searchInput.removeEventListener('blur', this.showTooltip)
// ipc.removeListener('top-save-all', this.saveAllHandler)
ipc.removeListener('top-focus-search', this.focusSearchHandler)
ipc.removeListener('top-new-post', this.newPostHandler)
} }
handleTooltipRequest (e) { handleTooltipRequest (e) {
@@ -67,7 +123,6 @@ export default class ArticleTopBar extends React.Component {
dispatch(clearSearch()) dispatch(clearSearch())
return return
} }
this.blurInput()
} }
focusInput () { focusInput () {
@@ -90,6 +145,32 @@ export default class ArticleTopBar extends React.Component {
this.focusInput() 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) { handleTutorialButtonClick (e) {
let { dispatch } = this.props let { dispatch } = this.props
@@ -99,9 +180,9 @@ export default class ArticleTopBar extends React.Component {
render () { render () {
let { status } = this.props let { status } = this.props
return ( return (
<div className='ArticleTopBar'> <div tabIndex='2' className='ArticleTopBar'>
<div className='left'> <div className='ArticleTopBar-left'>
<div className='search'> <div className='ArticleTopBar-left-search'>
<i className='fa fa-search fa-fw' /> <i className='fa fa-search fa-fw' />
<input <input
ref='searchInput' ref='searchInput'
@@ -114,25 +195,49 @@ export default class ArticleTopBar extends React.Component {
/> />
{ {
this.props.status.search != null && this.props.status.search.length > 0 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 : null
} }
<div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}> <div className={'tooltip' + (this.state.isTooltipHidden ? ' hide' : '')}>
- Search by tag : #{'{string}'}<br/> <ul>
- Search by folder : /{'{folder_name}'} <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>
</div> </div>
{status.isTutorialOpen ? searchTutorialElement : null} {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>
<div className='right'>
<div className='ArticleTopBar-right'>
<button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span> <button onClick={e => this.handleTutorialButtonClick(e)}>?<span className='tooltip'>How to use</span>
</button> </button>
<ExternalLink className='logo' href='http://b00st.io'> <a ref='links' className='ArticleTopBar-right-links-button' href>
<img src='../../resources/favicon-230x230.png' width='44' height='44'/> <img src='../resources/app.png' width='44' height='44'/>
<span className='tooltip'>Boost official page</span> </a>
</ExternalLink> {
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/boost-app-discussions/issues'>
<i className='fa fa-fw fa-bullhorn'/> Discuss
</ExternalLink>
</div>
)
: null
}
</div> </div>
{status.isTutorialOpen ? ( {status.isTutorialOpen ? (
@@ -140,7 +245,7 @@ export default class ArticleTopBar extends React.Component {
<div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/> <div onClick={e => this.handleTutorialButtonClick(e)} className='clickJammer'/>
<svg width='500' height='250' className='finder'> <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='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>
<svg width='450' className='global'> <svg width='450' className='global'>
<text x='100' y='45' fontSize='24' fill={BRAND_COLOR}>Hope you to enjoy our app :D</text> <text x='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 = { ArticleTopBar.propTypes = {
search: PropTypes.string,
dispatch: PropTypes.func, dispatch: PropTypes.func,
status: PropTypes.shape({ status: PropTypes.shape({
search: PropTypes.string search: PropTypes.string
}) }),
folders: PropTypes.array
} }

View File

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

View File

@@ -1,20 +1,24 @@
import React, { PropTypes} from 'react' import React, { PropTypes} from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { CREATE_MODE, EDIT_MODE, IDLE_MODE, NEW, toggleTutorial } from 'boost/actions' import ReactDOM from 'react-dom'
// import UserNavigator from './HomePage/UserNavigator' import { toggleTutorial } from '../actions'
import ArticleNavigator from './HomePage/ArticleNavigator' import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar' import ArticleTopBar from './ArticleTopBar'
import ArticleList from './HomePage/ArticleList' import ArticleList from './ArticleList'
import ArticleDetail from './HomePage/ArticleDetail' import ArticleDetail from './ArticleDetail'
import _ from 'lodash' import _ from 'lodash'
import keygen from 'boost/keygen' import { isModalOpen, closeModal } from 'browser/lib/modal'
import { isModalOpen, closeModal } from 'boost/modal'
const electron = require('electron')
const remote = electron.remote
const TEXT_FILTER = 'TEXT_FILTER' const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER' const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER' const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER' const TAG_FILTER = 'TAG_FILTER'
const OSX = global.process.platform === 'darwin'
class HomePage extends React.Component { class HomePage extends React.Component {
componentDidMount () { componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、 // React自体のKey入力はfocusされていないElementからは動かないため、
@@ -29,12 +33,16 @@ class HomePage extends React.Component {
handleKeyDown (e) { handleKeyDown (e) {
if (isModalOpen()) { if (isModalOpen()) {
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
remote.getCurrentWebContents().send('modal-confirm')
}
if (e.keyCode === 27) closeModal() if (e.keyCode === 27) closeModal()
return return
} }
let { status, dispatch } = this.props 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) { if (status.isTutorialOpen) {
dispatch(toggleTutorial()) dispatch(toggleTutorial())
@@ -42,95 +50,66 @@ class HomePage extends React.Component {
return return
} }
// Search inputがfocusされていたら大体のキー入力は無視される。 if (e.keyCode === 13 && top.isInputFocused()) {
if (top.isInputFocused() && !e.metaKey) { listElement.focus()
if (e.keyCode === 13 || e.keyCode === 27) top.escape() return
}
if (e.keyCode === 27 && top.isInputFocused()) {
if (status.search.length > 0) top.escape()
else listElement.focus()
return return
} }
switch (status.mode) { // Search inputがfocusされていたら大体のキー入力は無視される。
case CREATE_MODE: if (e.keyCode === 27) {
case EDIT_MODE: if (document.activeElement !== listElement) {
if (e.keyCode === 27) { listElement.focus()
detail.handleCancelButtonClick() } else {
} top.focusInput()
if ((e.keyCode === 13 && e.metaKey) || (e.keyCode === 83 && e.metaKey)) { }
detail.handleSaveButtonClick() return
}
break
case IDLE_MODE:
if (e.keyCode === 69) {
detail.handleEditButtonClick()
e.preventDefault()
}
if (e.keyCode === 68) {
detail.handleDeleteButtonClick()
}
// `detail`の`openDeleteConfirmMenu`の時。
if (detail.state.openDeleteConfirmMenu) {
if (e.keyCode === 27) {
detail.handleDeleteCancelButtonClick()
}
if (e.keyCode === 13 && e.metaKey) {
detail.handleDeleteConfirmButtonClick()
}
break
}
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
if (e.keyCode === 27 || (e.keyCode === 70 && e.metaKey)) {
top.focusInput()
}
if (e.keyCode === 38) {
list.selectPriorArticle()
}
if (e.keyCode === 40) {
list.selectNextArticle()
}
if (e.keyCode === 65 || e.keyCode === 13 && e.metaKey) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
} }
} }
render () { 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 ( return (
<div className='HomePage'> <div className='HomePage'>
<ArticleNavigator <ArticleNavigator
ref='nav' ref='nav'
dispatch={dispatch} dispatch={dispatch}
folders={folders}
status={status} status={status}
user={user}
folders={folders}
allArticles={allArticles} allArticles={allArticles}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/> />
<ArticleTopBar <ArticleTopBar
ref='top' ref='top'
dispatch={dispatch} dispatch={dispatch}
status={status} status={status}
folders={folders}
/> />
<ArticleList <ArticleList
ref='list' ref='list'
dispatch={dispatch} dispatch={dispatch}
folders={folders} folders={folders}
articles={articles} articles={articles}
status={status} modified={modified}
activeArticle={activeArticle} activeArticle={activeArticle}
/> />
<ArticleDetail <ArticleDetail
ref='detail' ref='detail'
dispatch={dispatch} dispatch={dispatch}
activeArticle={activeArticle}
folders={folders}
status={status} status={status}
tags={tags} tags={tags}
filters={filters} user={user}
folders={folders}
modified={modified}
activeArticle={activeArticle}
/> />
</div> </div>
) )
@@ -139,7 +118,7 @@ class HomePage extends React.Component {
// Ignore invalid key // Ignore invalid key
function ignoreInvalidKey (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 // Build filter object by key
@@ -156,12 +135,26 @@ function buildFilter (key) {
return {type: TEXT_FILTER, value: key} return {type: TEXT_FILTER, value: key}
} }
function remap (state) { function isContaining (target, needle) {
let { folders, articles, status } = state 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) => { 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() let allArticles = articles.slice()
@@ -170,6 +163,7 @@ function remap (state) {
return sum.concat(article.tags) return sum.concat(article.tags)
}, [])) }, []))
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
// Filter articles // Filter articles
let filters = status.search.split(' ') let filters = status.search.split(' ')
.map(key => key.trim()) .map(key => key.trim())
@@ -184,10 +178,10 @@ function remap (state) {
let targetFolders let targetFolders
if (folders != null) { if (folders != null) {
let exactTargetFolders = folders.filter(folder => { let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => folder.name.match(new RegExp(`^${filter.value}$`))) return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
}) })
let fuzzyTargetFolders = folders.filter(folder => { let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => folder.name.match(new RegExp(`^${filter.value}`))) return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
}) })
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders) targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
@@ -200,7 +194,7 @@ function remap (state) {
if (textFilters.length > 0) { if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => { articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => { return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i')) return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
}) })
}, articles) }, articles)
} }
@@ -208,7 +202,7 @@ function remap (state) {
if (tagFilters.length > 0) { if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => { articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => { return articles.filter(article => {
return _.find(article.tags, tag => tag.match(new RegExp(tagFilter.value, 'i'))) return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
}) })
}, articles) }, articles)
} }
@@ -218,74 +212,30 @@ function remap (state) {
let activeArticle = _.findWhere(articles, {key: status.articleKey}) let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0] if (activeArticle == null) activeArticle = articles[0]
// 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 { return {
user,
folders, folders,
status, status,
allArticles,
articles, articles,
allArticles,
modified,
activeArticle, activeArticle,
tags, tags
filters: {
folder: folderFilters,
tag: tagFilters,
text: textFilters
}
} }
} }
HomePage.propTypes = { HomePage.propTypes = {
params: PropTypes.shape({ status: PropTypes.shape(),
userId: PropTypes.string user: PropTypes.shape({
}), name: PropTypes.string
status: PropTypes.shape({
userId: PropTypes.string
}), }),
articles: PropTypes.array, articles: PropTypes.array,
allArticles: PropTypes.array, allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
dispatch: PropTypes.func, dispatch: PropTypes.func,
folders: PropTypes.array, folders: PropTypes.array,
filters: PropTypes.shape({ tags: PropTypes.array
folder: PropTypes.array,
tag: PropTypes.array,
text: PropTypes.array
})
} }
export default connect(remap)(HomePage) 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' import React, { PropTypes } from 'react'
import HomePage from './HomePage'
var ContactModal = require('boost/components/modal/ContactModal')
export default class MainContainer extends React.Component { export default class MainContainer extends React.Component {
constructor (props) { constructor (props) {
@@ -19,21 +19,13 @@ export default class MainContainer extends React.Component {
ipc.send('update-app', 'Deal with it.') ipc.send('update-app', 'Deal with it.')
} }
openContactModal () {
this.openModal(ContactModal)
}
render () { render () {
return ( return (
<div className='Main'> <div className='Main'>
{this.state.updateAvailable ? ( {this.state.updateAvailable ? (
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button> <button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
) : null} ) : null}
{/* <button onClick={this.openContactModal} className='contactButton'> <HomePage/>
<i className='fa fa-paper-plane-o'/>
<div className='tooltip'>Contact us</div>
</button> */}
{this.props.children}
</div> </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 // Action types
export const USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE' export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY' 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_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE' export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY' export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE' export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER' export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE' export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER' export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER' export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH' 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 TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
export const IDLE_MODE = 'IDLE_MODE'
export const CREATE_MODE = 'CREATE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
// Article status // Article status
export const NEW = 'NEW' export const NEW = 'NEW'
export function updateUser (input) {
return {
type: USER_UPDATE,
data: input
}
}
// DB // 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) { export function updateArticle (article) {
return { return {
type: ARTICLE_UPDATE, 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) { export function switchArticle (articleKey) {
return { return {
type: SWITCH_ARTICLE, 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 () { export function toggleTutorial () {
return { return {
type: TOGGLE_TUTORIAL 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 { Provider } from 'react-redux'
// import { updateUser } from 'boost/actions'
import { Router, Route, IndexRoute } from 'react-router'
import MainPage from './MainPage' import MainPage from './MainPage'
import HomePage from './HomePage' import store from './store'
// import auth from 'boost/auth' import React from 'react'
import store from 'boost/store'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
require('../styles/main/index.styl') require('../styles/main/index.styl')
import { openModal } from 'boost/modal' import { openModal } from 'browser/lib/modal'
import Tutorial from 'boost/components/modal/Tutorial' import Tutorial from './modal/Tutorial'
import activityRecord from 'boost/activityRecord' import activityRecord from 'browser/lib/activityRecord'
import ipc from 'ipc' const electron = require('electron')
const ipc = electron.ipcRenderer
const path = require('path')
const remote = electron.remote
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
activityRecord.init() activityRecord.init()
window.addEventListener('online', function () { window.addEventListener('online', function () {
ipc.send('check-update', 'check-update') ipc.send('check-update', 'check-update')
}) })
let routes = ( function notify (title, options) {
<Route path='/' component={MainPage}> if (process.platform === 'win32') {
<IndexRoute name='home' component={HomePage}/> options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
</Route> options.silent = false
) }
console.log(options)
return new window.Notification(title, options)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
ipc.on('copy-finder', function () {
activityRecord.emit('FINDER_COPY')
})
ipc.on('open-finder', function () {
activityRecord.emit('FINDER_OPEN')
})
let el = document.getElementById('content') let el = document.getElementById('content')
ReactDOM.render(( ReactDOM.render((
<div> <div>
<Provider store={store}> <Provider store={store}>
<Router>{routes}</Router> <MainPage/>
</Provider> </Provider>
</div> </div>
), el, function () { ), el, function () {

View File

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

@@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { getClientKey } from 'boost/activityRecord' import clientKey from 'browser/lib/clientKey'
import linkState from 'boost/linkState' import linkState from 'browser/lib/linkState'
import _ from 'lodash' import _ from 'lodash'
import { request, WEB_URL } from 'boost/api' import { request, SERVER_URL } from 'browser/lib/api'
const FORM_MODE = 'FORM_MODE' const FORM_MODE = 'FORM_MODE'
const DONE_MODE = 'DONE_MODE' const DONE_MODE = 'DONE_MODE'
@@ -34,7 +34,7 @@ export default class ContactTab extends React.Component {
handleSendButtonClick (e) { handleSendButtonClick (e) {
let input = _.pick(this.state, ['title', 'content', 'email']) let input = _.pick(this.state, ['title', 'content', 'email'])
input.clientKey = getClientKey() input.clientKey = clientKey.get()
this.setState({ this.setState({
alert: { alert: {
@@ -42,7 +42,7 @@ export default class ContactTab extends React.Component {
message: 'Sending...' message: 'Sending...'
} }
}, () => { }, () => {
request.post(WEB_URL + 'apis/inquiry') request.post(SERVER_URL + 'apis/inquiry')
.send(input) .send(input)
.then(res => { .then(res => {
console.log('sent') console.log('sent')

View File

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

View File

@@ -1,7 +1,7 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import FolderRow from './FolderRow' import FolderRow from './FolderRow'
import linkState from 'boost/linkState' import linkState from 'browser/lib/linkState'
import { createFolder } from 'boost/actions' import { createFolder } from '../../actions'
export default class FolderSettingTab extends React.Component { export default class FolderSettingTab extends React.Component {
constructor (props) { constructor (props) {
@@ -20,8 +20,6 @@ export default class FolderSettingTab extends React.Component {
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
this.setState({alert: null}, () => { this.setState({alert: null}, () => {
if (this.state.name.trim().length === 0) return false
let { dispatch } = this.props let { dispatch } = this.props
try { 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 React, { PropTypes } from 'react'
import MarkdownPreview from 'boost/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
export default class Tutorial extends React.Component { export default class Tutorial extends React.Component {
constructor (props) { constructor (props) {
@@ -80,7 +80,7 @@ export default class Tutorial extends React.Component {
Boost supports code syntax highlighting.<br/> Boost supports code syntax highlighting.<br/>
There are more than 100 different type of language. There are more than 100 different type of language.
<div className='code'> <div className='code'>
<CodeEditor readOnly mode='jsx' code={code}/> <CodeEditor readOnly article={{content:code, mode: 'jsx'}}/>
</div> </div>
</div> </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='title'>Easy to access with Finder</div>
<div className='content'> <div className='content'>
The Finder helps you organize all of the files and documents.<br/> 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/> It is available to save your articles on the Clipboard<br/>
by selecting your file with pressing Enter key,<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>
</div>) </div>)
case 4: 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 iptBgColor = #E6E6E6
iptFocusBorderColor = #369DCD iptFocusBorderColor = #369DCD
DEFAULT_FONTS = 'Lato', 'MS Gothic', 'Malgun Gothic', 'Sans-serif'
body body
font-family "Lato" font-family DEFAULT_FONTS
color textColor color textColor
font-size fontSize font-size fontSize
width 100% width 100%
height 100% height 100%
overflow hidden overflow hidden
button, input
font-family "Lato"
.Finder .Finder
absolute top bottom left right absolute top bottom left right
@@ -46,6 +50,7 @@ body
width 250px width 250px
overflow-y auto overflow-y auto
z-index 0 z-index 0
user-select none
&>ul>li &>ul>li
.articleItem .articleItem
padding 10px padding 10px
@@ -115,5 +120,9 @@ body
overflow-y auto overflow-y auto
.MarkdownPreview .MarkdownPreview
marked() marked()
&.empty
color lighten(inactiveTextColor, 10%)
user-select none
font-size 14px
.CodeEditor .CodeEditor
absolute top bottom left right absolute top bottom left right

View File

@@ -0,0 +1,390 @@
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 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%)
.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 width 250px
border-top 1px solid borderColor border-top 1px solid borderColor
border-right 1px solid borderColor border-right 1px solid borderColor
&:focus
border-color focusBorderColor
overflow-y auto overflow-y auto
noSelect() noSelect()
&>div &>div
.articleItem .ArticleList-item
border solid 2px transparent border solid 2px transparent
position relative position relative
height 88px height 110px
width 100% width 100%
cursor pointer cursor pointer
transition 0.1s transition 0.1s
background-color white background-color white
padding 0 10px padding 0 10px
font-size 12px font-size 12px
.top .ArticleList-item-top
clearfix() clearfix()
line-height 20px padding-top 2px
padding 5px 0 line-height 18px
height 20px
color articleItemColor color articleItemColor
font-size 11px
.folderName .folderName
overflow ellipsis overflow ellipsis
display inline-block display inline-block
width 120px width 120px
.updatedAt .updatedAt
float right float right
line-height 20px line-height 18px
.middle .unsaved-mark
padding 3px 0 7px color brandColor
.ArticleList-item-middle
font-size 16px font-size 16px
position relative position relative
height 26px padding-top 6px
height 22px
.mode .mode
position absolute position absolute
left 0 left 0
@@ -48,17 +54,31 @@ articleItemColor = #777
left 19px left 19px
right 0 right 0
overflow ellipsis overflow ellipsis
.bottom small
padding 5px 0 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
overflow-x auto overflow-x auto
white-space nowrap white-space nowrap
padding-top 6px
.tags .tags
color articleItemColor color articleItemColor
height 14px
a a
background-color brandColor background-color brandColor
color white color white
border-radius 2px border-radius 2px
padding 1.5px 5px padding 1px 5px
margin 2px margin 2px
font-size 10px font-size 10px
opacity 0.8 opacity 0.8

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 bgColor = #E6E6E6
inputBgColor = white inputBgColor = white
iptFocusBorderColor = #369DCD
topBarBtnColor = #B3B3B3 topBarBtnColor = #B3B3B3
topBarBtnBgColor = #B3B3B3 topBarBtnBgColor = #B3B3B3
@@ -15,6 +14,7 @@ infoBtnActiveBgColor = #3A3A3A
left 200px left 200px
height 60px height 60px
background-color bgColor background-color bgColor
user-select none
&>.tutorial &>.tutorial
.clickJammer .clickJammer
fixed top left bottom right fixed top left bottom right
@@ -36,14 +36,15 @@ infoBtnActiveBgColor = #3A3A3A
fixed top left bottom right fixed top left bottom right
z-index 20 z-index 20
background-color transparentify(black, 80%) background-color transparentify(black, 80%)
&>.left &>.ArticleTopBar-left
float left float left
&>.tutorial &>.tutorial
fixed top fixed top
left 200px left 100px
top 30px
z-index 36 z-index 36
font-style italic font-style italic
&>.search &>.ArticleTopBar-left-search
position relative position relative
float left float left
height 33px height 33px
@@ -62,6 +63,17 @@ infoBtnActiveBgColor = #3A3A3A
opacity 1 opacity 1
&.hide &.hide
opacity 0 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 input
absolute top left absolute top left
width 350px width 350px
@@ -75,7 +87,7 @@ infoBtnActiveBgColor = #3A3A3A
line-height 33px line-height 33px
z-index 0 z-index 0
&:focus &:focus
border-color iptFocusBorderColor border-color focusBorderColor
i.fa.fa-search i.fa.fa-search
position absolute position absolute
display block display block
@@ -84,7 +96,7 @@ infoBtnActiveBgColor = #3A3A3A
line-height 33px line-height 33px
z-index 1 z-index 1
pointer-events none pointer-events none
.searchClearBtn .ArticleTopBar-left-search-clear-button
position absolute position absolute
top 6px top 6px
right 10px right 10px
@@ -98,24 +110,56 @@ infoBtnActiveBgColor = #3A3A3A
line-height 20px line-height 20px
text-align center text-align center
padding 0 padding 0
&:focus
color textColor
&:hover &:hover
color white color white
background-color topBarBtnBgColor background-color topBarBtnBgColor
&>.refreshBtn &:active
color white
background-color darken(topBarBtnBgColor, 35%)
.ArticleTopBar-left-control
line-height 33px
float left float left
width 33px
height 33px height 33px
margin-top 13.5px margin-top 13.5px
margin-left 15px margin-left 20px
border none .tutorial
color refreshBtColor fixed top
background transparent left 200px
font-size 18px z-index 36
line-height 18px font-style italic
transition 0.1s button.ArticleTopBar-left-control-new-post-button
&:hover position fixed
color refreshBtnActiveColor background bgColor
&>.right 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 float right
&>button &>button
display block display block
@@ -129,28 +173,52 @@ infoBtnActiveBgColor = #3A3A3A
background-color infoBtnBgColor background-color infoBtnBgColor
color bgColor color bgColor
border-radius 11px border-radius 11px
border none border 1px solid bgColor
transition 0.1s transition 0.1s
&:focus
background-color lighten(infoBtnActiveBgColor, 15%)
.tooltip .tooltip
tooltip() tooltip()
margin-left -50px margin-left -50px
margin-top 29px margin-top 20px
&:hover &:hover
background-color infoBtnActiveBgColor background-color infoBtnActiveBgColor
.tooltip .tooltip
opacity 1 opacity 1
&>.logo &>.ArticleTopBar-right-links-button
display block display block
position absolute position absolute
top 8px top 8px
right 15px right 15px
opacity 0.7 opacity 0.7
.tooltip border-radius 23px
tooltip() height 46px
margin-top 44px width 46px
margin-left -120px border 1px solid transparent
&:focus
border-color focusBorderColor
&:hover &:hover
opacity 1 opacity 1
.tooltip .tooltip
opacity 1 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/*' @import '../mixins/*'
global-reset() global-reset()
@import '../shared/*' @import '../shared/*'
@import './components/*' @import './ArticleNavigator'
@import './containers/*' @import './ArticleTopbar'
@import './HomeContainer' @import './ArticleList'
@import './ArticleDetail'
@import './modal/*'
* DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
-webkit-app-region no-drag
-webkit-user-select none
html, body html, body
width 100% width 100%
@@ -17,13 +17,13 @@ html, body
overflow hidden overflow hidden
body body
font-family "Lato" font-family DEFAULT_FONTS
color textColor color textColor
font-size fontSize font-size fontSize
font-weight 400 font-weight 400
button, input, select button, input, select, textarea
font-family "Lato" font-family DEFAULT_FONTS
div, span, a, button, input, textarea div, span, a, button, input, textarea
box-sizing border-box box-sizing border-box
@@ -31,7 +31,7 @@ div, span, a, button, input, textarea
a a
color brandColor color brandColor
&:hover &:hover
color darken(brandColor, 15%) color lighten(brandColor, 5%)
&:visited &:visited
color brandColor color brandColor

View File

@@ -1,6 +1,7 @@
.EditedAlert.modal .DeleteArticleModal.modal
width 350px width 350px !important
top 100px top 100px
user-select none
.title .title
font-size 24px font-size 24px
margin-bottom 15px margin-bottom 15px
@@ -20,9 +21,13 @@
margin-left 5px margin-left 5px
&:hover &:hover
background-color darken(white, 10%) background-color darken(white, 10%)
&.primary &:focus
border-color brandColor border-color focusBorderColor
background-color brandColor &.danger
border-color #E9432A
background-color #E9432A
color white color white
&:hover &: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 left 180px
overflow-y auto overflow-y auto
&>.section &>.section
padding 10px padding 10px 20px
border-bottom 1px solid borderColor border-bottom 1px solid borderColor
overflow-y auto overflow-y auto
&:nth-last-child(1) &:nth-last-child(1)
@@ -69,17 +69,29 @@ iptFocusBorderColor = #369DCD
font-size 18px font-size 18px
margin 10px 0 5px margin 10px 0 5px
color brandColor color brandColor
&>.sectionInput &>.sectionCheck
margin-bottom 5px
height 33px 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 margin-bottom 5px
clearfix() clearfix()
height 33px
label label
width 180px width 150px
padding-left 15px padding-left 15px
float left float left
line-height 33px line-height 33px
input input
width 300px width 250px
float left float left
height 33px height 33px
border-radius 5px border-radius 5px
@@ -89,6 +101,51 @@ iptFocusBorderColor = #369DCD
outline none outline none
&:focus &:focus
border-color iptFocusBorderColor 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 &>.sectionConfirm
clearfix() clearfix()
padding 5px 15px padding 5px 15px
@@ -624,10 +681,3 @@ iptFocusBorderColor = #369DCD
color brandColor color brandColor
&:hover &:hover
color lighten(brandColor, 10%) color lighten(brandColor, 10%)

View File

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

View File

@@ -1,83 +1,137 @@
marked() marked()
h1, h2, h3, h4, h5, h6, p font-size 14px
&:first-child div.math-rendered
margin-top 0 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 hr
border-top none border-top none
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 15px 0 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 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 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 p
line-height 1.8em line-height 1.9em
margin 15px 0 25px margin 0 0 15px
img img
max-width 100% max-width 100%
strong strong, b
font-weight bold font-weight bold
em em, i
font-style italic font-style italic
s s, del, strike
text-decoration line-through text-decoration line-through
u
text-decoration underline
blockquote blockquote
border-left solid 4px brandBorderColor border-left solid 4px brandBorderColor
margin 15px 0 25px margin 0 0 15px
padding 0 25px padding 0 25px
ul ul
list-style-type disc list-style-type disc
padding-left 35px padding-left 25px
margin-bottom 35px margin-bottom 15px
li li
display list-item display list-item
line-height 1.8em line-height 1.8em
&>li>ul, &>li>ol
margin 0
&>li>ul &>li>ul
list-style-type circle list-style-type circle
&>li>ul &>li>ul
list-style-type square list-style-type square
ol ol
list-style-type decimal list-style-type decimal
padding-left 35px padding-left 25px
margin-bottom 35px margin-bottom 15px
li li
display list-item display list-item
line-height 1.8em line-height 1.8em
&>li>ul, &>li>ol
margin 0
code code
font-family monospace font-family Monaco, Menlo, 'Ubuntu Mono', Consolas, source-code-pro, monospace
padding 2px 4px padding 2px 4px
border solid 1px borderColor border solid 1px alpha(borderColor, 0.3)
border-radius 4px border-radius 4px
font-size 0.9em font-size 0.9em
color black color black
text-decoration none text-decoration none
background-color #F6F6F6 background-color #F6F6F6
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre pre
padding 5px padding 5px
border solid 1px borderColor border solid 1px alpha(borderColor, 0.3)
border-radius 5px border-radius 5px
overflow-x auto overflow-x auto
margin 15px 0 25px margin 0 0 15px
background-color #F6F6F6 background-color #F6F6F6
line-height 1.35em
&>code &>code
margin 0
padding 0 padding 0
border none border none
border-radius 0 border-radius 0

View File

@@ -3,6 +3,8 @@ highlightenBorderColor = darken(borderColor, 20%)
invBorderColor = #404849 invBorderColor = #404849
brandBorderColor = #3FB399 brandBorderColor = #3FB399
focusBorderColor = #369DCD
buttonBorderColor = #4C4C4C buttonBorderColor = #4C4C4C
lightButtonColor = #898989 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/'

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

View File

@@ -1,171 +0,0 @@
import React, { PropTypes } from 'react'
import _ from 'lodash'
import linkState from 'boost/linkState'
import api from 'boost/api'
export default class TeamSettingTab extends React.Component {
constructor (props) {
super(props)
let team = this.getCurrentTeam(props)
this.state = {
teamName: team != null ? team.profileName : '',
deleteConfirm: false,
alert: null
}
}
componentWillReceiveProps (nextProps) {
let team = this.getCurrentTeam(nextProps)
this.setState({
teamName: team != null ? team.profileName : '',
deleteConfirm: false
})
}
getCurrentTeam (props) {
if (props == null) props = this.props
return _.findWhere(props.teams, {id: props.currentTeamId})
}
handleTeamSelectChange (e) {
this.props.switchTeam(e.target.value)
}
handleSaveButtonClick (e) {
let input = {
profileName: this.state.teamName
}
let alert = {
type: 'info',
message: 'Sending...'
}
this.setState({alert}, () => {
api.updateTeamInfo(this.props.currentTeamId, input)
.then(res => {
console.log(res.body)
let alert = {
type: 'success',
message: 'Successfully done!'
}
this.setState({alert})
})
.catch(err => {
var message
if (err.status != null) {
message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.'
} else throw err
let alert = {
type: 'error',
message: message
}
this.setState({alert})
})
})
}
handleDeleteConfirmButtonClick (e) {
api.destroyTeam(this.props.currentTeamId)
.then(res => {
console.log(res.body)
})
.catch(err => {
let message
if (err.status != null) {
message = err.response.body.message
} else if (err.code === 'ECONNREFUSED') {
message = 'Can\'t connect to API server.'
} else throw err
console.log(message)
})
}
renderTeamOptions () {
return this.props.teams.map(team => {
return (
<option key={'team-' + team.id} value={team.id}>{team.name}</option>)
})
}
render () {
let team = this.getCurrentTeam()
if (team == null || team.userType === 'person') {
return this.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'>
<div className='sectionTitle'>Team profile</div>
<div className='sectionInput'>
<label className='label'>Team Name</label>
<input valueLink={this.linkState('teamName')} type='text'/>
</div>
<div className='sectionConfirm'>
<button onClick={e => this.handleSaveButtonClick(e)}>Save</button>
{this.state.alert != null
? (
<div className={'alert ' + this.state.alert.type}>{this.state.alert.message}</div>
)
: null}
</div>
</div>
{!this.state.deleteConfirm
? (
<div className='section teamDelete'>
<label>Delete this team</label>
<button onClick={e => this.setState({deleteConfirm: true})} className='deleteBtn'><i className='fa fa-fw fa-trash'/> Delete</button>
</div>
)
: (
<div className='section teamDeleteConfirm'>
<label>Are you sure to delete this team?</label>
<button onClick={e => this.setState({deleteConfirm: false})}>Cancel</button>
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='deleteBtn'><i className='fa fa-fw fa-check'/> Sure</button>
</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>
)
}
}
TeamSettingTab.propTypes = {
currentTeamId: PropTypes.number,
teams: PropTypes.array,
switchTeam: PropTypes.func
}
TeamSettingTab.prototype.linkState = linkState

View File

@@ -1,265 +0,0 @@
import React, { PropTypes } from 'react'
import { connect, Provider } from 'react-redux'
import linkState from 'boost/linkState'
import store from 'boost/store'
import AppSettingTab from './Preference/AppSettingTab'
import HelpTab from './Preference/HelpTab'
import FolderSettingTab from './Preference/FolderSettingTab'
import ContactTab from './Preference/ContactTab'
import { closeModal } from 'boost/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 { 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/>)
}
}
// handleProfileSaveButtonClick (e) {
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'info',
// message: 'Sending...'
// }
// this.setState({profile: profileState}, () => {
// let input = {
// profileName: profileState.userInfo.profileName,
// email: profileState.userInfo.email
// }
// api.updateUserInfo(input)
// .then(res => {
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'success',
// message: 'Successfully done!'
// }
// this.setState({profile: profileState})
// })
// .catch(err => {
// var message
// if (err.status != null) {
// message = err.response.body.message
// } else if (err.code === 'ECONNREFUSED') {
// message = 'Can\'t connect to API server.'
// } else throw err
// let profileState = this.state.profile
// profileState.userInfo.alert = {
// type: 'error',
// message: message
// }
// this.setState({profile: profileState})
// })
// })
// }
// handlePasswordSaveButton (e) {
// let profileState = this.state.profile
// if (profileState.password.newPassword !== profileState.password.confirmation) {
// profileState.password.alert = {
// type: 'error',
// message: 'Confirmation doesn\'t match'
// }
// this.setState({profile: profileState})
// return
// }
// profileState.password.alert = {
// type: 'info',
// message: 'Sending...'
// }
// this.setState({profile: profileState}, () => {
// let input = {
// password: profileState.password.currentPassword,
// newPassword: profileState.password.newPassword
// }
// api.updatePassword(input)
// .then(res => {
// let profileState = this.state.profile
// profileState.password.alert = {
// type: 'success',
// message: 'Successfully done!'
// }
// profileState.password.currentPassword = ''
// profileState.password.newPassword = ''
// profileState.password.confirmation = ''
// this.setState({profile: profileState})
// })
// .catch(err => {
// var message
// if (err.status != null) {
// message = err.response.body.message
// } else if (err.code === 'ECONNREFUSED') {
// message = 'Can\'t connect to API server.'
// } else throw err
// let profileState = this.state.profile
// profileState.password.alert = {
// type: 'error',
// message: message
// }
// profileState.password.currentPassword = ''
// profileState.password.newPassword = ''
// profileState.password.confirmation = ''
// this.setState({profile: profileState}, () => {
// if (this.refs.currentPassword != null) findDOMNode(this.refs.currentPassword).focus()
// })
// })
// })
// }
// renderProfile () {
// let profileState = this.state.profile
// return (
// <div className='content profile'>
// <div className='section userSection'>
// <div className='sectionTitle'>User Info</div>
// <div className='sectionInput'>
// <label>Profile Name</label>
// <input valueLink={this.linkState('profile.userInfo.profileName')} type='text'/>
// </div>
// <div className='sectionInput'>
// <label>E-mail</label>
// <input valueLink={this.linkState('profile.userInfo.email')} type='text'/>
// </div>
// <div className='sectionConfirm'>
// <button onClick={e => this.handleProfileSaveButtonClick(e)}>Save</button>
// {this.state.profile.userInfo.alert != null
// ? (
// <div className={'alert ' + profileState.userInfo.alert.type}>{profileState.userInfo.alert.message}</div>
// )
// : null}
// </div>
// </div>
// <div className='section passwordSection'>
// <div className='sectionTitle'>Password</div>
// <div className='sectionInput'>
// <label>Current Password</label>
// <input ref='currentPassword' valueLink={this.linkState('profile.password.currentPassword')} type='password' placeholder='Current Password'/>
// </div>
// <div className='sectionInput'>
// <label>New Password</label>
// <input valueLink={this.linkState('profile.password.newPassword')} type='password' placeholder='New Password'/>
// </div>
// <div className='sectionInput'>
// <label>Confirmation</label>
// <input valueLink={this.linkState('profile.password.confirmation')} type='password' placeholder='Confirmation'/>
// </div>
// <div className='sectionConfirm'>
// <button onClick={e => this.handlePasswordSaveButton(e)}>Save</button>
// {profileState.password.alert != null
// ? (
// <div className={'alert ' + profileState.password.alert.type}>{profileState.password.alert.message}</div>
// )
// : null}
// </div>
// </div>
// </div>
// )
// }
}
Preferences.propTypes = {
folders: PropTypes.array,
dispatch: PropTypes.func
}
Preferences.prototype.linkState = linkState
function remap (state) {
let { folders, status } = state
return {
folders,
status
}
}
let RootComponent = connect(remap)(Preferences)
export default class PreferencesModal extends React.Component {
render () {
return (
<Provider store={store}>
<RootComponent/>
</Provider>
)
}
}

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