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

Compare commits

...

743 Commits

Author SHA1 Message Date
Sota Sugiura
6a711d6a71 Merge pull request #476 from BoostIO/v8-8-8
v0.8.8
2017-04-22 18:30:05 +09:00
sota1235
9d8e71aeb3 v8.8.8 2017-04-22 16:34:58 +09:00
Sota Sugiura
cfeeba209e Merge pull request #468 from BoostIO/feature-fix-text
Fix text on menu bar(macOS) and some refactor
2017-04-22 16:31:58 +09:00
Sota Sugiura
455029851a Merge pull request #474 from BoostIO/delete-escbutton-in-initimodal
delete the escButton in the InitModal
2017-04-22 16:19:32 +09:00
Sota Sugiura
d0f7baaad0 Merge pull request #473 from BoostIO/Fix-ConfigTab-Interface
Fix the config tab layout
2017-04-22 16:17:43 +09:00
Kazu Yokomizo
e3fb236139 Fix the config tab layout 2017-04-22 16:06:01 +09:00
Sosuke Suzuki
42296e421a delete the escButton in the InitModal 2017-04-22 16:05:22 +09:00
Sota Sugiura
c6f4ed7c8f Merge pull request #472 from BoostIO/Design-update
Fix the english typo
2017-04-22 15:45:09 +09:00
Kazu Yokomizo
e1fb36d64d Fix the border-left width at folder-selector 2017-04-22 15:33:05 +09:00
Sota Sugiura
48cf695e11 Merge branch 'master' into Design-update 2017-04-22 15:17:03 +09:00
Kazu Yokomizo
5b0e0c71a0 Fix the english typo 2017-04-22 14:59:53 +09:00
Sota Sugiura
c1a76b6fb4 Merge pull request #290 from BoostIO/featureSyntaxHighlightCheck
Feature syntax highlight check
2017-04-22 13:20:19 +09:00
Sota Sugiura
945a6306ec Merge pull request #469 from LeoLamCY/add-support-to-bold-selected-text-via-kb-shortcut
Add support to bold selected text via kb shortcut
2017-04-22 13:07:04 +09:00
sota1235
313bacf9dc modify: fix for ESLint 2017-04-22 13:00:25 +09:00
Sota Sugiura
9027f48dda Merge pull request #453 from bubuzzz/feature/double-click-to-rename-tab
double click on tab to rename
2017-04-22 12:58:29 +09:00
Sota Sugiura
eea8f7cdf4 Merge pull request #425 from AWEEKJ/feature_markdown_note
Fix new line problem in list and task list
2017-04-22 12:52:53 +09:00
Leo Lam
ffef239aa7 fix minor build error 2017-04-21 23:40:52 -04:00
Leo Lam
50fc15feea add support to bold selected text via kb shortcut
to improve upon #301
ctrl + : to bold a single word under caret or bold selected text if some
text is selected (highlighted)

#463
2017-04-21 23:32:57 -04:00
Leo Lam
2d7a37c872 Merge remote-tracking branch 'refs/remotes/BoostIO/master' 2017-04-21 23:24:44 -04:00
sota1235
db468fc095 modify: some refactor 2017-04-22 11:54:05 +09:00
sota1235
9a9f0035c2 modify: add label for quit button on macOS 2017-04-22 11:51:06 +09:00
Sota Sugiura
b9270cd040 Merge pull request #349 from asmsuechan/iss-332
Fix the screen become black on closing Boostnote when a state of Boostnote is fullscreen on mac
2017-04-22 11:44:44 +09:00
Kazu Yokomizo
65b1bd18c4 Merge pull request #465 from BoostIO/Design-update
Fix the loading layout
2017-04-21 20:48:44 +09:00
Kazu Yokomizo
c87ecc3d40 Fix the Statusbar layout 2017-04-21 20:23:33 +09:00
Kazu Yokomizo
e39e1648f9 Fix the loading layout 2017-04-21 20:07:14 +09:00
Kazu Yokomizo
091d2618a2 Merge pull request #462 from BoostIO/Design-update
Design update
2017-04-21 18:21:16 +09:00
Kazu Yokomizo
d039b17715 modify: Fix the SideNavFilter layout 2017-04-21 16:13:45 +09:00
Kazu Yokomizo
e350dca72c modify: Fix the star-btn layout at NoteDetail 2017-04-21 15:48:57 +09:00
Kazu Yokomizo
c07e334f03 modify: Fix the tag style at NoteDetail 2017-04-21 15:32:23 +09:00
Kazu Yokomizo
1ccfd8e392 modify: Fix the tag-list layout at NoteItem 2017-04-21 15:00:09 +09:00
Kazu Yokomizo
d22c40f0a5 modify: Fix the tag layout at note list 2017-04-21 14:53:25 +09:00
Kazu Yokomizo
3d37db6e54 modify: Fix the hover layout at SideNav 2017-04-21 14:33:16 +09:00
Kazu Yokomizo
22bd92916b modify: Fix the navtoggle-btn layout 2017-04-21 14:24:22 +09:00
Kazu Yokomizo
de303cf072 modify: Fix the sort-by-btn layout 2017-04-21 14:20:21 +09:00
Kazu Yokomizo
b22aad0678 modify: Fix the navtoggle-btn layout 2017-04-21 14:02:48 +09:00
Kazu Yokomizo
bf02f9b256 modify: Fix the layout of new-post-btn and status-bar 2017-04-21 13:45:04 +09:00
SuenagaRyota
6440ab423e Merge pull request #457 from SalvatoreTosti/master
Updated initial pieces of text to read more clearly in English
2017-04-20 22:16:53 -05:00
Salvatore Tosti
fceab73226 Updated initial pieces of text to read more clearly in English 2017-04-19 17:57:42 -07:00
Thai Tran
fded0ad3e8 double click on tab to rename 2017-04-19 15:05:47 +10:00
Kazu Yokomizo
294eb64686 Merge pull request #451 from BoostIO/Fix-slack-link
Fix slack group invitation
2017-04-19 10:15:45 +09:00
Kazu Yokomizo
851b28c482 Fix slack group invitation 2017-04-19 10:10:35 +09:00
Kazu Yokomizo
63fa2f1149 Fix slack group invitaition 2017-04-19 10:10:05 +09:00
Kazu Yokomizo
816d8decbd Merge pull request #443 from SalvatoreTosti/master
Changed wording in build.md and debug.md
2017-04-18 12:04:39 +09:00
Salvatore Tosti
85b4eedcd6 Changed wording in build.md and debug.md
I've adjusted the wording in the build and debug files to flow better and make more sense in English.
2017-04-17 19:38:20 -07:00
Kazu Yokomizo
901f40a669 Merge pull request #442 from BoostIO/Fix-typo
Fix typo in Infotab
2017-04-18 10:24:07 +09:00
Kazu Yokomizo
bae5f202a5 Fix typo in Infotab 2017-04-18 10:20:39 +09:00
Kazu Yokomizo
131f6780c2 Merge pull request #441 from SalvatoreTosti/master
Updated readme file to read more cleanly in English
2017-04-18 10:10:53 +09:00
Salvatore Tosti
a0e067cacf Updated readme file to read more cleanly in English
Made some small styling tweaks for greater uniformity within the readme document.
Also reworded a few sections to read more clearly.
2017-04-17 12:38:43 -07:00
Kazu Yokomizo
8cdcf537be Merge pull request #439 from BoostIO/update-info
Update information.
2017-04-18 00:57:38 +09:00
Kazu Yokomizo
575f5f160c Update information. 2017-04-18 00:55:31 +09:00
Kazu Yokomizo
d8fbac584c Merge pull request #438 from BoostIO/Fix-readme
Fix readme
2017-04-18 00:47:34 +09:00
Kazu Yokomizo
b73e1b04dc Fix readme 2017-04-17 18:21:12 +09:00
Kazu Yokomizo
3dd49c287d Merge pull request #424 from BoostIO/Design-update
Design update at v0.8.8
2017-04-15 13:00:36 +09:00
Kazu Yokomizo
6ce6a7036b Fix typo 2017-04-15 12:33:16 +09:00
SuenagaRyota
c120633f14 Merge pull request #426 from asmsuechan/edit-contributing.md
Edit contributing.md
2017-04-14 19:33:04 -07:00
asmsuechan
6e84b24309 Edit contributing.md 2017-04-14 18:52:48 -07:00
Kazu Yokomizo
343e35bb54 Fix the select-btn layout at setting modal in white theme 2017-04-14 17:14:45 +09:00
Kazu Yokomizo
09cf94d807 Fix to outline:none 2017-04-14 17:10:37 +09:00
Hanju Jo
a5531e20f2 Fix new line problem of list and task list 2017-04-14 17:09:30 +09:00
Kazu Yokomizo
0f311120af Fix the layout of NoteItem at SideNav 2017-04-14 16:42:01 +09:00
Kazu Yokomizo
614506cada Fix the hover/active layout at menubar in white theme 2017-04-14 15:45:52 +09:00
Kazu Yokomizo
7891d14a0a Fix the hover/active layout at NoteItemSimple 2017-04-14 15:38:45 +09:00
Kazu Yokomizo
2df12b6891 Fix the hover layout at NoteNavFilter in white theme 2017-04-14 15:28:27 +09:00
Kazu Yokomizo
97b42d6be1 Fix the hover/active layout at NoteItem in white theme 2017-04-14 15:26:45 +09:00
Kazu Yokomizo
39baadeb04 Fix hover/active layout at NoteItem 2017-04-14 15:23:52 +09:00
Kazu Yokomizo
a6f5452a85 Change the button color at setting modal 2017-04-13 20:45:26 +09:00
Kazu Yokomizo
29dc3bd550 Fix the layout of nav-btn at setting modal 2017-04-13 12:14:36 +09:00
Kazu Yokomizo
e103605956 Change the border-left at setting modal 2017-04-13 12:05:10 +09:00
Kazu Yokomizo
c510c2e540 Change the add-storage-btn at setting modal 2017-04-13 12:04:51 +09:00
Kazu Yokomizo
41d65e4132 Change the border-width at folder list 2017-04-13 11:46:41 +09:00
SuenagaRyota
b6cb532568 Merge pull request #422 from asmsuechan/iss-421
Update katex to 0.7.1
2017-04-12 14:27:36 -07:00
asmsuechan
a034ea3a05 Update katex to 0.7.1 2017-04-12 12:55:30 -07:00
Kazu Yokomizo
adeb45a9ce Fix the storage-btn layout at SideNav 2017-04-12 23:10:29 +09:00
Kazu Yokomizo
4ada755793 Change the info at InfoTag modal 2017-04-12 22:22:22 +09:00
Kazu Yokomizo
c4be052a49 Change the info at infoModal 2017-04-12 20:17:06 +09:00
Kazu Yokomizo
54c2d7bac9 Delete the border-line at Statusbar 2017-04-12 16:14:00 +09:00
Kazu Yokomizo
233ab17992 Change the default background-color of ui-btn at white theme 2017-04-12 15:08:51 +09:00
Kazu Yokomizo
e251ec64dc Fix the layout of SideNavFilter 2017-04-12 14:59:47 +09:00
Kazu Yokomizo
32bd6d76ee Change the All Notes icon at SideNavFilter 2017-04-12 14:48:52 +09:00
Kazu Yokomizo
3fc17634aa Merge pull request #418 from BoostIO/Update-slack-invitation
Update slack invitation
2017-04-12 12:45:20 +09:00
Kazu Yokomizo
555d725e7b Update slack group invitation link 2017-04-12 12:42:54 +09:00
Kazu Yokomizo
35416796b5 Update slack group invitation link 2017-04-12 12:42:14 +09:00
SuenagaRyota
6c737fe25f Merge pull request #399 from asmsuechan/remove-a-shortcut-fot-bold
Remove Ctrl-B shortcut for bold
2017-04-11 12:54:29 -07:00
Kazu Yokomizo
1b58e320aa Change the font color at dark theme 2017-04-11 00:47:13 +09:00
Kazu Yokomizo
658a90bf15 Fix the layout at NoteItem 2017-04-10 23:08:06 +09:00
Kazu Yokomizo
d092e75f3c Delete the border line at Statusbar 2017-04-10 22:11:44 +09:00
asmsuechan
162fae19cc Remove Ctrl-B shortcut for bold 2017-04-08 14:31:18 -07:00
Kazu Yokomizo
58f5035ec6 Change the star background-color when hover at NoteDetail 2017-04-08 16:58:52 +09:00
Kazu Yokomizo
c5076e4e95 Change the star background-color when hover 2017-04-08 16:54:36 +09:00
Kazu Yokomizo
28ef1e625c Fix the rename folder modal layout 2017-04-08 16:49:35 +09:00
Kazu Yokomizo
97441ccacb Fix the box-shadow 2017-04-08 16:46:11 +09:00
Kazu Yokomizo
6b29aed6c4 Fix the search modal layout 2017-04-08 16:40:26 +09:00
Kazu Yokomizo
31eb9caee4 Fix the dark layout at NoteList 2017-04-08 16:03:53 +09:00
Kazu Yokomizo
64cf34e673 Fix the make note modal layout at white theme 2017-04-08 15:57:59 +09:00
Kazu Yokomizo
8996ebb819 Fix the make note modal layout at dark theme 2017-04-08 15:53:31 +09:00
Kazu Yokomizo
a36c62044b Fix the create folder modal layout at dark theme 2017-04-08 15:49:43 +09:00
Kazu Yokomizo
13418109ea Fix the create folder modal layout at white theme 2017-04-08 15:37:55 +09:00
Kohei TAKATA
fe7c05aaa5 Merge pull request #393 from BoostIO/feature-use-yarn
Using yarn
2017-04-08 12:51:48 +09:00
Kazu Yokomizo
116e27e0db Fix the textarea layout at Snippet note 2017-04-08 01:04:32 +09:00
Kazu Yokomizo
6b135afe1a Change the font-size at FolderSelect 2017-04-07 19:20:49 +09:00
Kazu Yokomizo
7a9c4951a2 Fix the tag layout of white theme at NoteDetail 2017-04-07 19:08:34 +09:00
Kazu Yokomizo
041a51a70f Fix the tag layout of dark theme at NoteDetail 2017-04-07 19:04:09 +09:00
Kazu Yokomizo
7b4ff9906e Change the note-title-icon position at NoteList 2017-04-07 18:51:34 +09:00
Kazu Yokomizo
11ab5c7598 Fix the tag layout of white theme at NoteList 2017-04-07 18:45:16 +09:00
Kazu Yokomizo
64d53c611b Fix the tag layout of dark theme at NoteList 2017-04-07 18:43:36 +09:00
Kazu Yokomizo
657d69a0fb Fix the dark theme layout at NoteList 2017-04-07 15:25:59 +09:00
Kazu Yokomizo
62d39e6715 Change the StorageList layout 2017-04-07 15:12:46 +09:00
Kazu Yokomizo
a27d8192ee Change the white theme layout of NoteList 2017-04-07 15:05:18 +09:00
SuenagaRyota
41b69afe03 Merge pull request #385 from BoostIO/Change-trashButton-size
Change the trashButton size
2017-04-06 23:04:20 -07:00
sota1235
54b5af0741 modify: update docs 2017-04-07 14:39:10 +09:00
sota1235
d4060f8a5a add: using yarn 🎉 2017-04-07 14:37:13 +09:00
SuenagaRyota
2935d41aba Merge pull request #386 from clone1612/productionPruneFix
Fix for 'npm prune --production' deleting needed dependencies
2017-04-06 22:33:16 -07:00
Kazu Yokomizo
af6cd10e28 Change the layout at SideNav 2017-04-07 14:24:38 +09:00
SuenagaRyota
435c80d870 Merge pull request #392 from asmsuechan/rm-finder-menu
Remove finder-menu
2017-04-06 21:30:04 -07:00
asmsuechan
775cec32da Remove finder-menu
it's no longer required
2017-04-06 21:22:25 -07:00
SuenagaRyota
e8cc7abadc Merge pull request #364 from Hastegan/master
Uniform the accelerator notation
2017-04-06 21:08:55 -07:00
Kazu Yokomizo
c1051afdc0 Change the SideNav layout 2017-04-07 00:38:49 +09:00
Kazu Yokomizo
573d3ce11e Fix the layout on NoteList 2017-04-06 23:25:38 +09:00
Kazu Yokomizo
0a89dcc6d8 Change the font-size at SideNav 2017-04-06 23:09:24 +09:00
Kazu Yokomizo
d45033ae8e Change the font-size at NoteList 2017-04-06 22:59:09 +09:00
Kazu Yokomizo
313e8b8c98 Fix the dark theme layout of Sidebar and NoteList 2017-04-06 22:11:59 +09:00
Jannick Hemelhof
d6a78dfe28 Fix for prune deleting needed packages 2017-04-05 14:53:51 +02:00
Kazu Yokomizo
de45852790 Change the trashButton size 2017-04-05 11:38:44 +09:00
Kazu Yokomizo
c6a505cb44 Merge pull request #384 from BoostIO/update-slack-invitiaon
Update slack invitiaon
2017-04-05 11:36:10 +09:00
Kazu Yokomizo
44427a40b7 Update readme
Update slack group invitation link.
2017-04-05 11:32:06 +09:00
Kazu Yokomizo
8a2ac08c0a Update readme
Update the slack group invitation link.
2017-04-05 11:31:12 +09:00
Kazu Yokomizo
7babf66d5f Merge pull request #340 from asmsuechan/remove-a-menu-on-delete
Remove a menu which is shown on clicked the trash button
2017-04-05 11:26:18 +09:00
hastegan
6daffbcafa uniform shortcut notation 2017-03-29 01:10:55 +02:00
Sota Sugiura
b76729e836 Merge pull request #362 from BoostIO/Edit-slackgrou-link
Edit slack group invitation link
2017-03-28 15:14:49 +09:00
Kazu Yokomizo
b2294e0fc9 modify: Edited the slack group invitation link
at Japanese
2017-03-28 14:49:21 +09:00
Kazu Yokomizo
3559737e8e modify: Edited the slack group invitation link 2017-03-28 14:48:15 +09:00
asmsuechan
40b7ce607b Add it to snippet note 2017-03-27 11:49:54 -07:00
asmsuechan
1de67a00cb Remove a menu which is shown on clicked the trash button 2017-03-27 11:44:42 -07:00
SuenagaRyota
6b4b44dba8 Merge pull request #358 from asmsuechan/modify-documents
Modify and add documents
2017-03-27 02:37:44 -07:00
asmsuechan
c424cc5d33 Change the sentence 2017-03-26 23:55:09 -07:00
asmsuechan
5b71d010b4 Add docs/ja/debug.md 2017-03-26 23:49:18 -07:00
asmsuechan
b9457d3e33 Add docs/debug.md 2017-03-26 23:49:08 -07:00
asmsuechan
60e267409c Modify ja/build.md accompanied by npm run dev-start 2017-03-26 23:20:04 -07:00
asmsuechan
e6a2521143 Modify build.md accompanied by npm run dev-start 2017-03-26 23:15:36 -07:00
Sota Sugiura
ae36ed2b46 Merge pull request #357 from asmsuechan/add-concurrently
Add concurrently for running local environment more easily
2017-03-27 14:30:21 +09:00
asmsuechan
fb2ed81fd3 Add concurrently for running local environment more easily 2017-03-26 22:04:47 -07:00
Sosuke Suzuki
a74651b515 load javascript mode of codemirror & clean up dirty codes 2017-03-25 15:32:45 +09:00
SuenagaRyota
6904c192e4 Merge pull request #352 from redcom/fix-288
fix: #288 Fixed Snippet tabs overwriting other tabs when closed
2017-03-23 13:41:54 -07:00
Razvan Moraru
6540d2670c better formatting 2017-03-23 21:01:49 +01:00
Razvan Moraru
966ba06bc4 change let to const 2017-03-23 17:50:39 +01:00
Razvan Moraru
3b4921b848 fix: #288 Fixed Snippet tabs overwriting other tabs when closed 2017-03-23 09:20:02 +01:00
SuenagaRyota
b11e10ac07 Merge pull request #347 from BoostIO/add-slack-group-link
Add slack group link
2017-03-22 18:50:03 -07:00
asmsuechan
2ec7ba04f5 Fix by lint 2017-03-22 00:21:49 -07:00
asmsuechan
095910d156 Fix the screen become black on closing Boostnote when a state of Boostnote is fullscreen on mac 2017-03-22 00:00:45 -07:00
Kazu Yokomizo
c39463aea8 modify: add the join to slack group link
Ver Japanese
2017-03-21 15:54:29 +09:00
Kazu Yokomizo
87e47c7ffb modify: Add the join to slack group link 2017-03-21 15:52:43 +09:00
Sota Sugiura
359f6734c5 Merge pull request #317 from BoostIO/feature-add-esilnt-rule
Add prefer-const rule and fix code
2017-03-21 15:16:48 +09:00
sota1235
1d3e71cf49 fix: for ESLint 2017-03-21 15:14:03 +09:00
Sota Sugiura
1ab449cecf Merge branch 'master' into feature-add-esilnt-rule 2017-03-21 15:04:14 +09:00
Sota Sugiura
44dd609134 Merge pull request #289 from kostaldavid8/master
Smart bullets
2017-03-21 15:02:25 +09:00
kostaldavid8
1e8e161a33 remove console.log 2017-03-20 19:42:14 +01:00
kostaldavid8
c56d232e58 Added + as bullet, bug fix 2017-03-20 19:22:31 +01:00
Sota Sugiura
8315b75587 Merge pull request #342 from sota1235/feature-v0-8-7
v0.8.7
2017-03-20 16:11:24 +09:00
sota1235
562b0592af v0.8.7 2017-03-20 15:39:34 +09:00
Sota Sugiura
1fd1bed01a Merge pull request #343 from asmsuechan/remove-markdown-it-toc
Remove markdown-it-toc because an errors are happening
2017-03-20 15:38:32 +09:00
asmsuechan
4f116cba34 Remove markdown-it-toc because an errors are happening 2017-03-19 23:32:51 -07:00
Sota Sugiura
9d1d57f183 Merge pull request #331 from asmsuechan/fix-the-behavior-of-lock
Fix the behavior of a feature which locks the editor
2017-03-20 15:31:44 +09:00
asmsuechan
6feeee8933 Refactor the styles of the buttons 2017-03-19 22:17:01 -07:00
asmsuechan
5541c0dc38 Refactor styles of the buttons 2017-03-19 22:11:02 -07:00
asmsuechan
4a8054faed Fix the design of the trash button on snippet note 2017-03-19 22:04:19 -07:00
asmsuechan
bbced7be25 Fix the background-color of button 2017-03-19 21:56:41 -07:00
asmsuechan
4055ce19cd Fix the design of buttons of TopBar on dark theme 2017-03-19 11:18:17 -07:00
asmsuechan
bcf27233bc Fix a style of the lock button 2017-03-19 01:31:28 -07:00
asmsuechan
45111e1610 Change the styleName info-right-button to control-trashButton because it's not good name
* fix the position of lock button
2017-03-19 01:20:04 -07:00
asmsuechan
2af86dfa3e Fix the action of hover 2017-03-19 01:02:47 -07:00
Sota Sugiura
1b474e1c28 Merge pull request #337 from BoostIO/Change-the-tray-icon
Changed the tray icon when mouse on.
2017-03-19 16:45:15 +09:00
Sota Sugiura
c4370694cc Merge pull request #295 from asmsuechan/add-a-shortcut-for-jump-to-top
Add a shortcut which jumps to top by Ctrl-G
2017-03-19 16:43:15 +09:00
asmsuechan
91f24d96b9 Fix a bug which cannot read a property when moves to other folder on locking 2017-03-19 00:41:55 -07:00
Kazu Yokomizo
bb0b74e889 Changed the tray icon when mouse on. 2017-03-19 16:20:55 +09:00
asmsuechan
525ab900bd Fix == to === 2017-03-19 00:18:09 -07:00
asmsuechan
31c04de7b6 Change the name list:top to list:jumpToTop 2017-03-19 00:18:09 -07:00
asmsuechan
dea0c4287b Fix let to const 2017-03-19 00:18:09 -07:00
asmsuechan
cec4b3132c Add a shortcut which jumps to top by Ctrl-G 2017-03-19 00:18:05 -07:00
asmsuechan
f3ed22dd51 Enable to appear a tooltip for the LockButton 2017-03-18 23:19:43 -07:00
asmsuechan
6aa9104076 Fix not to be shown the lock button if config.editor.switchPreview is
RIGHTCLICK
2017-03-18 23:19:43 -07:00
asmsuechan
e7fd18967b Change a state name editorStatus to isLockButtonShown 2017-03-18 23:19:43 -07:00
asmsuechan
8a5558db55 Change a name showlockbutton to togglelockbutton 2017-03-18 23:19:43 -07:00
asmsuechan
4767f15e9b Fix the behavior of a feature what locks the editor 2017-03-18 23:19:43 -07:00
Sota Sugiura
b7ca4668e9 Merge pull request #301 from asmsuechan/add-a-shortcut-for-supporting-to-input-bold
Add a shortcut for supporting to input bold
2017-03-19 15:08:17 +09:00
Sota Sugiura
70e637fada Merge pull request #326 from asmsuechan/refactor-typos
Fix typos
2017-03-19 14:09:29 +09:00
SuenagaRyota
459b0ff030 Merge pull request #316 from asmsuechan/fix-the-design-of-li
Fix the design of li
2017-03-18 10:58:30 -07:00
SuenagaRyota
2903788fd4 Merge pull request #330 from BoostIO/feature-dragDrop-svg
dragged svg file is turns not into xml text
2017-03-17 22:54:04 -07:00
Sosuke Suzuki
af0fdb9277 move the line under L57 2017-03-18 14:49:46 +09:00
SuenagaRyota
41a58583dc Merge pull request #324 from BoostIO/feature_note_title_markdown_sharp
ignore # in the code block
2017-03-17 21:48:16 -07:00
Sosuke Suzuki
c80a26fe0b dragged svg file is turns not into xml text 2017-03-18 13:44:35 +09:00
Sosuke Suzuki
806c3bbaf9 delete unnecessary } and correct the indent 2017-03-18 12:27:43 +09:00
Sosuke Suzuki
fe1c197138 reduce indent 2017-03-18 03:02:04 +09:00
Sosuke Suzuki
b577ca2bc2 Refactor the dirty code 2017-03-18 01:58:15 +09:00
asmsuechan
70a97a6a2a Fix a typo 2017-03-16 13:30:24 -07:00
Sosuke Suzuki
034f46792b ignore # in the code block 2017-03-17 03:42:17 +09:00
SuenagaRyota
3dc1b59753 Merge pull request #307 from BoostIO/feature-trashCan-icon
I changed the button  deleting snnipet and markdown from ... to trash can.
2017-03-15 13:55:31 -07:00
SuenagaRyota
98b761f1d1 Merge pull request #322 from BoostIO/featureSuccessfullyDeleteSubtab
The snippet subtab was successfully deleted.
2017-03-15 13:51:11 -07:00
Sosuke Suzuki
712301436d I crrected indents 2017-03-16 02:54:57 +09:00
Sosuke Suzuki
4243afb033 remove the extra file, and correct indents 2017-03-16 02:34:26 +09:00
Sosuke Suzuki
0f43485606 The snippet subtab was successfully deleted. 2017-03-16 01:36:58 +09:00
Sota Sugiura
b91b88f16e Merge pull request #321 from BoostIO/Change-the-tray-icon
Change the tray icons.
2017-03-14 14:24:02 +09:00
Kazu Yokomizo
2f2c500e4a Change the tray icons. 2017-03-14 13:59:59 +09:00
sota1235
7065fad69b modify: change warning level for no-lone-blocks rule 2017-03-13 18:55:16 +09:00
sota1235
6fcbca6b10 refactor: by ESLint 2017-03-13 18:40:12 +09:00
sota1235
93b15f2a7a modify: change warning level for no-undef 2017-03-13 18:36:21 +09:00
sota1235
df9d8ff735 modify: change warning level for no-unused-var 2017-03-13 18:33:39 +09:00
sota1235
fda17e044e modify: change warning level for prefer-const rule 2017-03-13 18:26:56 +09:00
sota1235
b4e54fc149 modify: npm script 2017-03-13 18:26:39 +09:00
asmsuechan
48514d1020 Change an arg name md to mdElement 2017-03-12 16:07:40 -07:00
asmsuechan
49a4ec5e16 Enable CTRL + B in a word to make it bold 2017-03-12 16:04:08 -07:00
asmsuechan
c3e92b3b81 Fix if clauses 2017-03-12 16:04:00 -07:00
asmsuechan
e78492983a Add a shortcut for supporting to input bold 2017-03-12 16:02:52 -07:00
sota1235
fac0abaed6 modify: add prefer-const rule and fix code 2017-03-10 10:12:23 +09:00
asmsuechan
a6bd239592 Fix the design of li
* fix the design of markdown-it-TOC
2017-03-09 13:04:14 -08:00
SuenagaRyota
7845bbd881 Merge pull request #310 from Gansgar/master
Added Table of Contents and two small bug fixes
2017-03-09 12:09:50 -08:00
SuenagaRyota
68bc440749 Merge pull request #298 from asmsuechan/fix-drop-an-image-on-CodeEditor-and-MarkdownPreview
Disable dragging an image to MarkdownPreview and fix the behavior of an image dropped on CodeEditor.
2017-03-08 15:38:27 +09:00
SuenagaRyota
6dbe3cec69 Merge pull request #283 from asmsuechan/add-lock-to-CodeEditor
Enable lock in MarkdownEditor
2017-03-07 11:16:55 +09:00
georg
850c339bb3 bug fix to select all
- at least on OSX fixed bug, that would select all text when clicking,
opening or any other action was done
2017-03-04 18:37:04 +01:00
georg
57835d0e32 Added ToC support
- added `markdown-it-toc-and-anchor` into the project
- integrated it, create a table of contents every in a markup by adding
`@[TOC]`
2017-03-04 17:48:17 +01:00
georg
ac2c50c8bc Fixed background of taskListItem
- fixed background bug when using a taskListItem with the light design
2017-03-04 17:18:30 +01:00
Kohei TAKATA
7296cbe4ec Merge pull request #309 from BoostIO/feature-v0-8-6
v0.8.6
2017-03-04 14:06:31 +09:00
Kohei TAKATA
16061a7eba v0.8.6 2017-03-04 13:04:18 +09:00
Sosuke Suzuki
566fe92589 change the icon to delete snippet and markdown 2017-03-03 02:13:13 +09:00
asmsuechan
83cef13f1c Change let to const 2017-02-21 23:30:37 +09:00
asmsuechan
4bb9533049 Fix prevention of an image dropped on MarkdownPreview and fix the behavior of an image dropped on CodeEditor 2017-02-21 00:14:03 +09:00
kostaldavid8
d07c62e266 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2017-02-18 23:36:09 +01:00
SuenagaRyota
8beb661af4 Merge pull request #291 from kostaldavid8/image-drag-fix
Image drag fix
2017-02-18 21:06:16 +09:00
Sosuke Suzuki
5a201dd1b9 correction the pointed out code 2017-02-18 12:26:38 +09:00
asmsuechan
aa0ad3bb70 Fix from review 2017-02-18 12:18:55 +09:00
kostaldavid8
f7fb531902 Fixed image drag and drop
Added escaping and changed function that wasn't working
2017-02-17 10:31:41 +01:00
kostaldavid8
c65db4e2b0 Fixed image drag and drop
Added escaping and changed function that wasn't working
2017-02-17 10:04:55 +01:00
Sota Sugiura
b32b38bb0d Merge pull request #287 from ericsolomon/274_font_color_rename_folder_modal
Fix font color for dark theme rename folder modal
2017-02-16 21:38:37 -08:00
kostaldavid8
d6171dc502 Smart numbered lists, too
Has auto increment, but no auto indent on tab, I don't know what to do
2017-02-16 13:18:17 +01:00
Sosuke Suzuki
7b5a7aabed add HighLightCheckEditor 2017-02-16 13:00:45 +09:00
kostaldavid8
77eb19af40 Code style fixes 2017-02-15 23:30:56 +01:00
kostaldavid8
e68d535fa2 Smart bullets
When you hit enter on a line with a bullet, you get a new one on the new line.
Also when you hit tab after a bullet, it automatically indents.
It makes typing with bullets much more pleasant.
2017-02-15 22:18:20 +01:00
Eric Solomon
6e5f6cc739 Fix font color for dark theme rename folder modal 2017-02-14 03:21:23 -06:00
asmsuechan
bbeeeccb31 Fix to use by preventDefault 2017-02-11 23:09:03 +09:00
asmsuechan
e9525fae22 Change the order of focus and emit 'editor:lock' 2017-02-11 23:09:01 +09:00
asmsuechan
672d409bf2 Enable to show the lock icon only the Editor state is CODE 2017-02-11 17:52:11 +09:00
Kohei TAKATA
6624178864 Merge pull request #284 from BoostIO/feature-v0-8-5
v0.8.5
2017-02-11 17:47:30 +09:00
Kohei TAKATA
4a66c6717c v0.8.5 2017-02-11 16:08:31 +09:00
Kohei TAKATA
dd76bc027b Merge pull request #282 from BoostIO/fix-windows-taskbar-icon
Fix windows taskbar icon
2017-02-11 16:02:58 +09:00
asmsuechan
74ee6ae6ce Add lock icon to NoteDetail 2017-02-11 15:48:38 +09:00
Kohei TAKATA
1ae3f295f3 Remove unused variable 2017-02-11 15:47:25 +09:00
Kohei TAKATA
3cb2ce41fe Merge pull request #278 from EmEpsilon/fix-239-on-windows
refs #239 [Fix] cannot open finder with using hotkey on windows
2017-02-11 15:38:21 +09:00
Kohei TAKATA
5534319e93 Fix windows taskbar icon 2017-02-11 14:05:14 +09:00
EmEpsilon
c7373c15a5 Fix: cannot open finder with using hotkey 2017-02-10 00:35:31 +09:00
Sota Sugiura
dbf1d6403b Merge pull request #216 from asmsuechan/decode-for-codemirror
Decodes HTML entity in code area
2017-02-08 05:44:49 -08:00
Sosuke Suzuki
743b220953 unfinished syntaxHighLightCheck 2017-02-04 14:41:26 +09:00
Sota Sugiura
95d74c6f5b Merge pull request #269 from sota1235/feature-v0-8-4
v0.8.4
2017-01-28 14:22:42 +09:00
sota1235
f04b7db9fc v0.8.4 2017-01-28 13:54:54 +09:00
Sota Sugiura
900fa023fb Merge pull request #261 from asmsuechan/change-Ctrl-E-customizable
refs #260 Change a shortcut Ctrl-E to Ctrl-W
2017-01-28 13:37:08 +09:00
asmsuechan
ad9da44afb Move a shortcut escapeFromCodeEditor from CodeEditor to MarkdownEditor, because it's not customizable 2017-01-28 13:27:34 +09:00
asmsuechan
c827717202 refs #260 Change a shortcut Ctrl-E to Ctrl-W
it escapes CodeEditor to MarkdownPreview
2017-01-28 12:51:49 +09:00
Sota Sugiura
7d3caa3c2e Merge pull request #267 from asmsuechan/fix-ctrl-e-shortcut
Fix Ctrl + E shortcut on escape from CodeEditor
2017-01-28 12:48:32 +09:00
asmsuechan
fde7fbccac Fix Ctrl + E shortcut on escape from CodeEditor 2017-01-27 01:27:29 +09:00
Sota Sugiura
56f06fa7d5 Merge pull request #262 from BoostIO/Changed-the-order-of-English-and-Japanese
Changed the order of English and Japanese.
2017-01-26 11:24:07 +09:00
Sota Sugiura
c0fba82e73 Merge pull request #265 from BoostIO/delete-readme-ko
Delete readme-ko.md
2017-01-26 11:23:34 +09:00
Kohei TAKATA
5438cd14a0 Merge pull request #257 from sota1235/feature-fix_style_for_side_nav
Fix style for side nav
2017-01-25 19:20:47 +09:00
Sota Sugiura
0d642b308d Merge pull request #264 from BoostIO/add-maintainer-1
add-maintainer(ja)
2017-01-25 01:29:52 +09:00
Sota Sugiura
0b96472f72 Merge pull request #259 from BoostIO/add-maintainer
Add maintainer
2017-01-25 01:29:37 +09:00
Kazu Yokomizo
675d0ed08c Delete readme-ko.md 2017-01-25 01:29:24 +09:00
Kazu Yokomizo
9c0f5c31c2 add-maintainer(ja) 2017-01-25 01:22:19 +09:00
Kazu Yokomizo
09ce59fd04 Deleted (English) 2017-01-25 01:14:54 +09:00
Kazu Yokomizo
98cd83c4e0 Changed the order of English and Japanese. 2017-01-24 19:18:52 +09:00
Kazu Yokomizo
1aec386656 Add maintainer 2017-01-21 18:45:28 +09:00
sota1235
b03cd9cd99 modify: fix style for side nav 2017-01-21 17:34:02 +09:00
Sota Sugiura
27265e210f Merge pull request #256 from BoostIO/release-v0-8-3
v0.8.3
2017-01-21 16:51:45 +09:00
sota1235
c392c5d178 v0.8.3 2017-01-21 16:32:27 +09:00
Sota Sugiura
11fe420fac Merge pull request #255 from BoostIO/featureEscButton
Feature esc button
2017-01-21 16:30:45 +09:00
Sota Sugiura
9b17a8fb5b Merge pull request #250 from asmsuechan/add-drop-image
Dropping images into CodeEditor
2017-01-21 16:28:56 +09:00
Sota Sugiura
27f3fd0032 Merge pull request #241 from asmsuechan/iss-208
Fixes syntax hilight of inline code on Preview
2017-01-21 16:14:58 +09:00
Sota Sugiura
1672d9fa5f Merge pull request #235 from asmsuechan/add-focus-shortcut
Adds shortcuts
2017-01-21 16:12:13 +09:00
asmsuechan
59c9e11879 Change the color in <code> tag on preview 2017-01-21 16:12:03 +09:00
asmsuechan
4523743150 Fix by review
refs: https://github.com/BoostIO/Boostnote/pull/235#pullrequestreview-17800321
2017-01-21 16:06:14 +09:00
Sosuke Suzuki
2fdbe9de96 unify finder backgroundcolor 2017-01-21 16:04:55 +09:00
asmsuechan
a617976c78 Fix by review
refs: https://github.com/BoostIO/Boostnote/pull/250#pullrequestreview-17801164
2017-01-21 15:57:42 +09:00
Sota Sugiura
7201a98d78 Merge pull request #245 from asmsuechan/add-md-and-text-exporter
Add export as txt/md
2017-01-21 15:54:01 +09:00
Sosuke Suzuki
472560e2bf correction EscButton in darkmode 2017-01-21 15:52:06 +09:00
Sota Sugiura
96753fe0a0 Merge pull request #254 from BoostIO/featureFinderBackgroundColor
unify finder backgroundcolor
2017-01-21 15:51:15 +09:00
Sosuke Suzuki
83c2fdd161 unify finder backgroundcolor 2017-01-21 15:44:52 +09:00
asmsuechan
911ce7572f Fix by review
refs: https://github.com/BoostIO/Boostnote/pull/245#pullrequestreview-17800372
2017-01-21 15:44:48 +09:00
Sota Sugiura
0a24d7d4a7 Merge pull request #247 from asmsuechan/iss-246
Fix a Bug when using the Finder Window with markdown Notes
2017-01-21 15:05:51 +09:00
Sota Sugiura
c542062d4d Merge pull request #253 from BoostIO/featureEscButton
changeEscButton
2017-01-21 14:55:13 +09:00
Sosuke Suzuki
eb7a195cce NewNoteModal fineModification 2017-01-21 14:50:10 +09:00
Sosuke Suzuki
23a356164e changeEscButton 2017-01-21 14:31:38 +09:00
asmsuechan
de19c51061 Change to use Object.assign by checking keyPressed and remove unnecessary processing 2017-01-21 01:04:33 +09:00
asmsuechan
221b6a2938 Alert users try to export md/txt in SNIPPET 2017-01-20 18:56:33 +09:00
asmsuechan
cda9d53c8e Add image dropper 2017-01-19 01:32:06 +09:00
Sota Sugiura
f043b0ffb3 Merge pull request #248 from whizark/more-zoom-options
modify: add more zoom options (140-200%)
2017-01-19 00:23:07 +09:00
Sota Sugiura
28e0590327 Merge pull request #244 from asmsuechan/hotfix-an-error-occurs-on-update
Fix a bug due to event handler of before-quit on update
2017-01-19 00:15:56 +09:00
asmsuechan
ec6de1b91b Fix typo 2017-01-19 00:01:14 +09:00
asmsuechan
2b0bdbf1c8 Fix pointed part by review 2017-01-18 12:03:29 +09:00
asmsuechan
f48864a2e7 Fix pointed part by review 2017-01-18 10:09:12 +09:00
Whizark
94c6578675 modify: add more zoom options (140-200%)
* https://github.com/BoostIO/Boostnote/issues/31#issuecomment-267843725
2017-01-18 02:52:26 +09:00
asmsuechan
2af2399971 refs #247 Add fixed blur handler 2017-01-17 11:27:37 +09:00
asmsuechan
ebea01cecf refs #246 Fix a Bug when using the Finder Window with markdown Notes 2017-01-16 22:17:25 +09:00
asmsuechan
5d1db1de31 Add export as txt/md 2017-01-16 13:09:59 +09:00
asmsuechan
6c528625d8 Fix a bug due to event handler of before-quit on update 2017-01-16 04:36:23 +09:00
sota1235
7b326b99af v0.8.2 2017-01-15 18:37:39 +09:00
asmsuechan
2a60ba95e0 refs #208 Fixes syntax hilight of inline code on Preview 2017-01-14 20:45:53 +09:00
Sota Sugiura
6b98afaa02 Merge pull request #238 from BoostIO/update-todo-link
Update to readme
2017-01-14 20:39:06 +09:00
asmsuechan
cdb079dc81 refs #226 Enables to use multiple key for shortcut 2017-01-14 20:26:43 +09:00
asmsuechan
2ac0d93caf refs #226 Adds Control to Hint of Hotkey 2017-01-14 19:17:47 +09:00
asmsuechan
41977e8726 refs #226 Adds shortcuts which move next note and prior note to main-menu 2017-01-14 19:17:43 +09:00
asmsuechan
b9e6a56a83 refs #226 Changes to toggle by Ctrl + S 2017-01-14 19:17:41 +09:00
asmsuechan
2468c8311f refs #226 Adds focus to search shortcut to main-menu 2017-01-14 19:17:39 +09:00
asmsuechan
e8e05b20cd refs #226 Adds blur shortcut on Editor 2017-01-14 19:17:35 +09:00
asmsuechan
5bd0a446f1 refs #226 Adds a shortcut which focuses the note 2017-01-14 19:17:24 +09:00
sota1235
ad4e50d542 hack: for ESLint 2017-01-14 18:15:48 +09:00
Sota Sugiura
13131a0d5c Merge pull request #240 from asmsuechan/iss-239
refs #239 Fixes a bug which cannot open finder with using HotKey
2017-01-14 18:11:59 +09:00
asmsuechan
f007664745 refs #239 Fixes a bug which cannot open finder with using HotKey 2017-01-14 18:04:54 +09:00
sota1235
87f9589be3 v0.8.1 2017-01-14 17:09:45 +09:00
Kazu Yokomizo
e059106a93 update-ja-readme 2017-01-14 17:08:35 +09:00
Kazu Yokomizo
ada1b4de6b update-todo-link 2017-01-14 17:05:58 +09:00
Sota Sugiura
96413b9851 Merge pull request #218 from asmsuechan/feature-vim-keymap
Adds vim keymap setting
2017-01-14 17:03:20 +09:00
Sota Sugiura
9699ef6319 Merge pull request #237 from sota1235/feature-add_folder_on_note_detail
Add folder selector on note detail.
2017-01-14 16:57:00 +09:00
sota1235
dd8f4d60f0 fix: do not ignore dist file 2017-01-14 16:49:38 +09:00
asmsuechan
372f2e7319 Adds a prop keyMap to NoteDetail and SnippetNoteDetail 2017-01-14 16:39:50 +09:00
sota1235
1957d87dd7 modify: fix font size - folder name 2017-01-14 15:52:11 +09:00
sota1235
a148d17ba1 modify: add folder selector on top of note detail component 2017-01-14 15:44:28 +09:00
sota1235
297553c240 modify: move updated string to the bottom of note detail component 2017-01-14 14:54:08 +09:00
sota1235
f0fcaa6be7 modify: move zoom-pointer to left on note detail component 2017-01-14 14:47:06 +09:00
sota1235
cfa40f3ec1 refactor: remove unused component 2017-01-14 14:40:33 +09:00
sota1235
832c43de88 refactor: remove unused option 2017-01-14 14:36:26 +09:00
sota1235
1665e18edb fix: remove unused npm command 2017-01-14 13:52:10 +09:00
Sota Sugiura
fd1717046b Merge pull request #232 from BoostIO/simplified
I simplified.
2017-01-14 13:36:47 +09:00
sota1235
7fe7c555bc modify: add badge for TravisCI 2017-01-14 13:30:42 +09:00
Sota Sugiura
411a7a8e80 Merge pull request #233 from BoostIO/update-more-information
Update more information.
2017-01-14 13:27:38 +09:00
sota1235
196f5a7bf7 modify: add license section 2017-01-14 13:24:25 +09:00
Kazu Yokomizo
3fa326121a Update more information. 2017-01-12 20:35:53 +09:00
Kazu Yokomizo
cce69bea3a I simplified. 2017-01-12 20:29:20 +09:00
Sota Sugiura
f70cf7845d Merge pull request #231 from sota1235/master
[Refactor] Use ESLint instead of StandarJS and add settings for TravisCI
2017-01-12 00:48:05 +09:00
Sota Sugiura
bd0a326128 Merge pull request #20 from sota1235/feature-settings_for_travis
Settings for TravisCI
2017-01-12 00:34:44 +09:00
sota1235
897d99e043 modify: add badge of TravisCI on README 2017-01-12 00:28:10 +09:00
sota1235
b0f288e103 fix: ignore invalid tests 2017-01-12 00:23:44 +09:00
sota1235
7d26d46c7b modify: execute test on TravisCI 2017-01-12 00:01:12 +09:00
sota1235
5c7804fc40 modify: fix some codes pointed by eslint 2017-01-11 23:59:48 +09:00
sota1235
836f3af1ab add: files for using eslint 2017-01-11 23:52:22 +09:00
sota1235
67b89d4fe7 modfiy: install npm libraries for using eslint 2017-01-11 23:49:59 +09:00
sota1235
bc2d9d0fe2 modify: add settings for travisci 2017-01-11 23:38:43 +09:00
sota1235
79f33b9405 refactor: fix some coding style pointed by standard js 2017-01-11 23:35:02 +09:00
sota1235
ed9ddee5f1 refactor: fix by standardjs 2017-01-11 23:17:32 +09:00
sota1235
0d004b2f0a add: npm script for fix codes with standardjs 2017-01-11 23:13:50 +09:00
sota1235
f41ff77d76 add: npm command for checking coding style 2017-01-11 23:12:24 +09:00
sota1235
ae97a76d2e add: for using TravisCI 2017-01-11 23:10:09 +09:00
Sota Sugiura
3ca18c04c6 Merge pull request #221 from asmsuechan/remember-window-size
refs #207 Keeps window size
2017-01-11 23:03:01 +09:00
asmsuechan
2b03e6e956 refs #207 Fixes lengthy assignment 2017-01-11 22:56:12 +09:00
Sota Sugiura
010793a478 Merge pull request #228 from asmsuechan/iss-224
refs #224 Fixes the style of folder-list
2017-01-11 22:30:03 +09:00
asmsuechan
b136512ece refs #224 Fixes the style of folder-list 2017-01-11 10:21:53 +09:00
asmsuechan
9179c199fe refs #207 Keeps window size 2017-01-09 11:37:05 +09:00
Sota Sugiura
cfa4dfa817 Merge pull request #222 from asmsuechan/fix-npm-install
Fixes npm task bacause oh-my-cdn is removed
2017-01-09 11:35:13 +09:00
asmsuechan
6a73a3af97 Fixes npm task bacause oh-my-cdn is removed 2017-01-09 11:27:55 +09:00
Sota Sugiura
923c24fa6c Merge pull request #214 from asmsuechan/master
Fixes the theme of code block by codemirror
2017-01-08 23:53:10 +09:00
Sota Sugiura
4b1c8a3238 Merge pull request #206 from sota1235/hotfix-replace_old_url
Replace old url.
2017-01-08 23:46:54 +09:00
sota1235
76508fbc3b refactor: remove oh-my-cdn 2017-01-08 23:45:34 +09:00
sota1235
2bfda95ed8 modify: get flowchart.js from npm 2017-01-08 23:44:49 +09:00
Sota Sugiura
6e5082a470 Merge pull request #220 from BoostIO/Fixed-the-features-url(ja)
Fixed the features url(ja).
2017-01-08 19:47:32 +09:00
Kazu Yokomizo
a6cec44fc4 Fixed the features url(ja). 2017-01-08 19:45:45 +09:00
Sota Sugiura
600fab4f23 Merge pull request #219 from BoostIO/Fixed-the-features-url
Fixed the features url.
2017-01-08 19:45:26 +09:00
Kazu Yokomizo
12377b8caf Fixed the features url. 2017-01-08 19:44:46 +09:00
Sota Sugiura
250c6e488d Merge pull request #211 from BoostIO/Update-Author-&-Maintainer
Update Author & Maintainer
2017-01-08 19:42:35 +09:00
Kazu Yokomizo
3a9b57adae Merge branch 'master' into Update-Author-&-Maintainer 2017-01-08 19:41:31 +09:00
Sota Sugiura
74415956ac Merge pull request #213 from BoostIO/Updated-of-Japanese-readme
Updated of Japanese readme.
2017-01-08 19:36:08 +09:00
Sota Sugiura
33d7ed25a5 Merge pull request #212 from BoostIO/Fixed-link-and-typo
Fixed link and typo.
2017-01-08 19:33:05 +09:00
asmsuechan
df2de5c081 Adds note of caution next to keymap change select box 2017-01-08 18:16:32 +09:00
asmsuechan
7f4c58a84a Adds vim keymap setting 2017-01-08 17:52:52 +09:00
asmsuechan
a641a7b3e4 Decodes HTML entity in code area 2017-01-08 12:29:43 +09:00
asmsuechan
7437b26e3c Fixes in order to handle default setting 2017-01-07 23:51:37 +09:00
asmsuechan
ee6d41859f Fixes the theme of code block by codemirror 2017-01-07 22:48:37 +09:00
Kazu Yokomizo
b368c3b5d8 Updated of Japanese readme.
Link, typo and list of Author & Maintainer
2017-01-07 19:33:31 +09:00
Kazu Yokomizo
b1ae2b0b6f Fixed link and typo. 2017-01-07 19:26:36 +09:00
Kazu Yokomizo
27a6d53c7f Update Author & Maintainer
add sota1235, Kohei TAKATA and Kazu Yokomizo.
2017-01-07 18:07:23 +09:00
sota1235
4caf3a81be fix: update the url of official homepage 2017-01-05 00:08:18 +09:00
sota1235
ab578f768f refactor: remove unused file 2017-01-05 00:06:03 +09:00
Sota Sugiura
e4212e796a Merge pull request #204 from sota1235/master
Use npm instead of using oh-my-cdn
2017-01-04 23:55:57 +09:00
Sota Sugiura
95b1ff9b41 Merge pull request #19 from sota1235/feature-remove_oh_my_cdn
Remove oh-my-cdn
2017-01-04 23:53:50 +09:00
sota1235
684d2c411e fix: path for react-dom.min.js 2017-01-04 23:51:29 +09:00
sota1235
42710cfee5 refactor: remove libraries in package.json 2017-01-04 23:46:06 +09:00
sota1235
277004fd9b refactor: replace with files in node_modules 2017-01-04 23:30:15 +09:00
sota1235
14f79c4c21 modify: install libraries with using npm, not using oh-my-cdn 2017-01-04 23:15:32 +09:00
Sota Sugiura
f88cd80dca Merge pull request #198 from sota1235/master
v0.8.0
2017-01-03 18:14:15 +09:00
sota1235
19ada1dbf6 v0.8.0 2017-01-03 18:13:12 +09:00
sota1235
7754ab1a2e modify: update readem for using new design 2017-01-03 18:11:47 +09:00
sota1235
de0a8837eb add: image file for readme 2017-01-03 18:09:15 +09:00
Sota Sugiura
042d059d53 Merge pull request #15 from sota1235/feature-design_renewal_rebase
Design renewal (v0.8.0)
2017-01-03 17:57:10 +09:00
sota1235
cba743c895 modify: fix sidenav style for folded mode 2017-01-03 17:56:30 +09:00
sota1235
24ee71ac06 modify: fix top for storage list 2017-01-03 17:51:00 +09:00
Sota Sugiura
0ec4ef3363 Merge pull request #18 from sota1235/hotfix-updated_string
Fix for updated string.
2017-01-03 17:33:31 +09:00
sota1235
98120a5e40 modify: fix style for updated date on note detail 2017-01-03 17:32:25 +09:00
Sota Sugiura
3cb2a6bf92 Merge pull request #17 from sota1235/hotfix-for_review
Some fix specified by @kazup.
2017-01-03 17:03:08 +09:00
sota1235
4b7262cb72 fix: text color on creating new folder form (dark theme) 2017-01-03 17:01:04 +09:00
sota1235
eac8b13d7b modify: fix padding for storage note count 2017-01-03 16:58:44 +09:00
sota1235
4458c58066 fix: scrolling for folder list 2017-01-03 16:56:52 +09:00
sota1235
245d603ae8 fix: Hello, 2017! 2017-01-03 16:54:07 +09:00
Sota Sugiura
e3d959522b Merge pull request #16 from sota1235/hotfix-border_for_hint_button
Fix style for hint button.
2017-01-03 16:30:58 +09:00
sota1235
124544452b fix: border-radius for hint button 2017-01-03 16:29:41 +09:00
sota1235
4534625084 modify: fix style for h1, h2 tag on markdown preview 2017-01-03 16:16:06 +09:00
sota1235
3e4342eec4 modify: remove underline for h tag on markdown previe 2017-01-03 16:16:06 +09:00
sota1235
0ed2d26129 fix: for renewal 2017-01-03 16:16:06 +09:00
sota1235
1b538993db modify: design for modal window creating new folder (dark theme) 2017-01-03 16:16:05 +09:00
sota1235
d67e4009e7 modify: change design for model window creating new folder 2017-01-03 16:16:05 +09:00
sota1235
7f066c4443 modify: fix some parameter for whole components 2017-01-03 16:16:05 +09:00
sota1235
2594ca984a modify: change style for snippet note on finder component 2017-01-03 16:16:05 +09:00
sota1235
0e089fadfb refactor: move component to common components directory 2017-01-03 16:16:05 +09:00
sota1235
e56518e13d modify: replace the component with common component 2017-01-03 16:16:05 +09:00
sota1235
c9bc0c89ff refactor: cut down the component from SideNav component 2017-01-03 16:16:05 +09:00
sota1235
b18b1171e7 refactor: replace storage item component with common component 2017-01-03 16:16:05 +09:00
sota1235
d65464401c refactor: replace component with common component 2017-01-03 16:16:05 +09:00
sota1235
e9a9e10c81 modify: cut down the component from SideNav component 2017-01-03 16:16:05 +09:00
sota1235
825cd6a93b modify: change design for note list on finder 2017-01-03 16:16:05 +09:00
sota1235
276471979a modify: move components file 2017-01-03 16:16:05 +09:00
sota1235
d6903edac7 modify: fix for black theme 2017-01-03 16:16:05 +09:00
sota1235
4e1b4bdd6a fix: border design for note item list 2017-01-03 16:16:05 +09:00
sota1235
52f0a5639d fix: markup for box-shadow 2017-01-03 16:16:05 +09:00
sota1235
58181d02b2 modify: change color for escape button on config modal 2017-01-03 16:16:05 +09:00
sota1235
1fea39f1b7 modify: add esc button for config modal 2017-01-03 16:16:05 +09:00
sota1235
ae97ff0f98 modify: fix some colors for config 2017-01-03 16:16:05 +09:00
sota1235
b2d7fa9e97 modify: fix style for button on storages config 2017-01-03 16:16:05 +09:00
sota1235
93e9235bb2 modify: markup style for info tab on config modal 2017-01-03 16:16:05 +09:00
sota1235
094bce20e2 modfiy: markup for UI config tab. 2017-01-03 16:16:05 +09:00
sota1235
2f9d4c447a modify: markup style for Hotkey config modal 2017-01-03 16:16:05 +09:00
sota1235
84eb790d93 modify: extend common config 2017-01-03 16:16:05 +09:00
sota1235
ebd07694db add: common style for config tabs 2017-01-03 16:16:05 +09:00
sota1235
2bbac8a6f4 modify: markup for storages config 2017-01-03 16:16:05 +09:00
sota1235
e74f5e835f modify: fix style for dark theme 2017-01-03 16:16:05 +09:00
sota1235
69e753cc71 modify: use new components insted of ConfigTab component 2017-01-03 16:16:05 +09:00
sota1235
b0978c772e modify: devide ConfigTab component for two components 2017-01-03 16:16:05 +09:00
sota1235
fb4dfbadf3 modify: change layout for config modal 2017-01-03 16:16:05 +09:00
sota1235
d19ff3ff17 modify: add shadow on notelist and topbar component(light theme) 2017-01-03 16:16:05 +09:00
sota1235
d0990be856 modify: fix font color for tags 2017-01-03 16:16:05 +09:00
sota1235
268d66b51d modify: add icon for NoteItem component 2017-01-03 16:16:05 +09:00
sota1235
c1df311e86 modify: add icon for NoteItemSimple component 2017-01-03 16:16:05 +09:00
sota1235
cc3bd41df2 modify: change text-align for title of the snipet files 2017-01-03 16:16:05 +09:00
sota1235
856979b455 modify: add border line on NoteList component 2017-01-03 16:16:05 +09:00
sota1235
47925489fd modify: not showing star when the note is not starred 2017-01-03 16:16:05 +09:00
sota1235
8aefb21123 modify: remove border color of slidebar 2017-01-03 16:16:05 +09:00
sota1235
e27751c18c modify: remove border-right of SideNav component 2017-01-03 16:16:05 +09:00
sota1235
a3f3fdcc71 modify: change base color for dark theme 2017-01-03 16:16:04 +09:00
sota1235
a757576920 fix: get js-sequence-diagrams from npm, not CDN 2017-01-03 16:16:04 +09:00
sota1235
c4eb28d241 modify: change style for NoteItemSimple component 2017-01-03 16:15:45 +09:00
sota1235
aba6c2eb4f add: style sheet for NoteItemSimple component 2017-01-03 16:15:45 +09:00
sota1235
ecb91b3155 refactor: remove unused styles 2017-01-03 16:15:45 +09:00
sota1235
7374b1cc70 refactor: replace code with micro components 2017-01-03 16:15:45 +09:00
sota1235
e8f2972659 modify: remove duplicated codes 2017-01-03 16:15:45 +09:00
sota1235
5bef19a306 add: stylesheet for note-item components 2017-01-03 16:15:45 +09:00
sota1235
54d9e02a42 add: NoteItem component on NoteList component when using simple display mode 2017-01-03 16:15:45 +09:00
sota1235
4f479a8baf add: NoteItem component on NoteList component 2017-01-03 16:15:45 +09:00
sota1235
e7e6194cac modify: not showing the star on the note-list component when using the simple mode 2017-01-03 16:15:45 +09:00
sota1235
113abbb94d refactor: remove unused component 2017-01-03 16:15:45 +09:00
sota1235
d9d0651352 modify: adjust space on note-detail component 2017-01-03 16:15:45 +09:00
sota1235
e27af9f6c1 fix: style for the TagSelect component 2017-01-03 16:15:45 +09:00
sota1235
11d820356d modify: change style for note-detail component (snipet mode) 2017-01-03 16:15:45 +09:00
sota1235
0945aab232 modify: fix text color on note list(dark theme) 2017-01-03 16:15:45 +09:00
sota1235
e4744221ee modify: fix the style of the tooltip for creating new note 2017-01-03 16:15:45 +09:00
sota1235
5f71b24f8d modify: change background-color for note-detail component 2017-01-03 16:15:45 +09:00
sota1235
095a29972a modify: fix text on the modal window for creating new note 2017-01-03 16:15:45 +09:00
sota1235
76bdb708fa refactor: move the code for note info to NoteDetailInfo.styl 2017-01-03 16:15:45 +09:00
sota1235
7c3c08fd96 modify: add color variable for note-detail component 2017-01-03 16:15:45 +09:00
sota1235
a4a2e09429 modify: remove tooltips 2017-01-03 16:15:45 +09:00
sota1235
4ed0ae5e2d modify: change icon for controling viewing mode of note list 2017-01-03 16:15:45 +09:00
sota1235
4cb5e43357 modify: change icon for All Notes 2017-01-03 16:15:45 +09:00
sota1235
b0fecc6b51 modify: fix style for dark theme 2017-01-03 16:15:45 +09:00
sota1235
dd4236ca89 modify: add Star on note right side 2017-01-03 16:15:45 +09:00
sota1235
5da908c759 modfiy: change style for note list 2017-01-03 16:15:45 +09:00
sota1235
fcce1d406d modify: move variables to global file 2017-01-03 16:15:45 +09:00
sota1235
5a01f39dc7 modfiy: fix style for sort button 2017-01-03 16:15:45 +09:00
sota1235
ea1d76f853 modfiy: change style for top bar 2017-01-03 16:15:45 +09:00
sota1235
2356d8a64f modify: add color variable for style 2017-01-03 16:15:45 +09:00
sota1235
969f82b903 modify: remove bordertop of note list 2017-01-03 16:15:45 +09:00
sota1235
fb90907abf modify: change ui-backgroundColor 2017-01-03 16:15:45 +09:00
sota1235
cbd4cd940c modify: fix padding-left on side nav bar 2017-01-03 16:15:45 +09:00
sota1235
6b18c6182e modify: fix style for storage item 2017-01-03 16:15:45 +09:00
sota1235
09164aa0c9 modify: fix space on side nav 2017-01-03 16:15:45 +09:00
sota1235
b83dadddb7 modify: remove border-bottom of menu button 2017-01-03 16:15:45 +09:00
sota1235
b3263b41ff Revert "fix: default.css not found"
This reverts commit 7b55454a73b3eebbfe1ed4684157c2d822ee2f05.
2017-01-03 16:15:45 +09:00
sota1235
1118149b9e modify: remove add tag button 2017-01-03 16:15:45 +09:00
sota1235
b6fc24c6e7 modify: fix style for more option button 2017-01-03 16:15:45 +09:00
sota1235
441edf4667 add: show last updated date on detail top bar 2017-01-03 16:15:45 +09:00
sota1235
74068eaa3d modify: fix test and logic 2017-01-03 16:15:45 +09:00
sota1235
c11bd9e7eb fix: default.css not found 2017-01-03 16:15:45 +09:00
sota1235
b21a82ea6b modify: add date string for detail 2017-01-03 16:15:45 +09:00
sota1235
cf455a13d5 add: util method for generating date 2017-01-03 16:15:45 +09:00
sota1235
c492f3529e modify: remove tooltip of 'more option' button 2017-01-03 16:15:45 +09:00
sota1235
40d7ba4bcc modify: remove border from options button 2017-01-03 16:15:45 +09:00
sota1235
686bc49230 modify: change color for adding tag button 2017-01-03 16:15:45 +09:00
sota1235
6550af698a modify: add button for tag component 2017-01-03 16:15:45 +09:00
sota1235
aac5cbf53e modify: fix active color for favorite star button 2017-01-03 16:15:45 +09:00
sota1235
00636db87c modify: remove animatino for removing tag button 2017-01-03 16:15:45 +09:00
sota1235
6231b8ad57 modify: fix size of removet tag button 2017-01-03 16:15:45 +09:00
sota1235
f2a41aa049 modify: change design for star button 2017-01-03 16:15:45 +09:00
sota1235
bb87b80a92 modify: add variable for star button's color 2017-01-03 16:15:45 +09:00
sota1235
ebfbe29217 modify: change design for tag 2017-01-03 16:15:45 +09:00
sota1235
fad837e148 modify: remove tag icon 2017-01-03 16:15:45 +09:00
sota1235
7dc84c0d6d revert: add FolderSelect Component again 2017-01-03 16:15:45 +09:00
sota1235
74807fe251 Revert "remove: unused file"
This reverts commit db1d0fb5d57967e586bd3e2cb02268f69a718861.
2017-01-03 16:15:45 +09:00
sota1235
bf9773be20 modify: move star on left side 2017-01-03 16:15:45 +09:00
sota1235
18961ff555 refactor: remove context button on top bar 2017-01-03 16:15:45 +09:00
sota1235
1f6e0342d6 modify: fix min width of window 2017-01-03 16:15:45 +09:00
sota1235
7d2bc58ba2 modify: change height of menu on SideNav component 2017-01-03 16:15:45 +09:00
sota1235
556b53181f modify: fix to use navWidth instead of listWidth 2017-01-03 16:15:45 +09:00
sota1235
ca904d69e5 modify: add comment for handleMousUp action 2017-01-03 16:15:45 +09:00
sota1235
f461d459d2 modify: change width of SideNav component 2017-01-03 16:15:45 +09:00
sota1235
6e535c11fd refactor: separate styl file 2017-01-03 16:15:45 +09:00
sota1235
49d3262380 remove: unused file 2017-01-03 16:15:45 +09:00
sota1235
1ee3dec0cc modify: fix style for detail info 2017-01-03 16:15:44 +09:00
sota1235
15637642bb refactor: remove FolderSelect Component 2017-01-03 16:15:44 +09:00
sota1235
3c950c2b9e refactor: remove ShareButton 2017-01-03 16:15:44 +09:00
Sota Sugiura
7811039651 Merge pull request #196 from asmsuechan/master
URL is no longer available
2016-12-31 00:39:16 +09:00
asmsuechan
ec5f7b38d0 Updates invalid url of sequence-diagram 2016-12-30 23:14:25 +09:00
sota1235
2bb361dc19 v0.7.5 2016-12-20 17:41:42 +09:00
Sota Sugiura
3445e484ae Merge pull request #188 from less-easy-way/patch-1
Just fix a trivial typo.
2016-12-14 02:43:39 +09:00
Jae-woo Kim
efd6bf2afe Just fix a trivial typo.
Just fixed a trivial typo.
어쩌다 발견하게 됐네요. :)
2016-12-13 23:28:50 +09:00
sota1235
cd2e6e1b24 v0.7.4 2016-12-13 20:33:04 +09:00
sota1235
99b8d24db3 hotfix: add dependent libraries 2016-12-13 20:24:13 +09:00
sota1235
8116b569c1 v0.7.3 2016-12-13 13:48:03 +09:00
sota1235
da791c5fed fix: link for boostnote store 2016-12-13 13:36:56 +09:00
sota1235
fbd9c59bfd modify: add contributor 2016-12-13 12:55:25 +09:00
Junyoung Choi
3a1b3d19c5 Merge pull request #166 from bkjohnson/master
Adding menu back in temporarily to fix shortcuts.
2016-10-28 12:15:03 +09:00
Brooks Johnson
238076f534 Adding menu back in temporarily to fix shortcuts. 2016-10-27 20:48:01 -05:00
Junyoung Choi
214d74ae11 Merge pull request #164 from bkjohnson/master
Minor accelerator updates
2016-10-27 21:05:01 +09:00
Brooks Johnson
30324f6113 Refactors color changes into mixin 2016-10-26 23:43:01 -05:00
Brooks Johnson
68f0a25873 Adds dark theme to controls in config window. 2016-10-26 20:39:54 -05:00
Brooks Johnson
cd5bc4e930 Simplify OSX accelerators (see here: http://electron.atom.io/docs/api/accelerator/#platform-notice) 2016-10-26 19:51:35 -05:00
Brooks Johnson
1d8f729c95 Update accelerators to use var instead of function 2016-10-26 19:37:44 -05:00
Dick Choi
f0d2fb53d4 add sequence diagram 2016-10-26 13:06:02 +09:00
Dick Choi
0445c680ba debounce rendering 2016-10-26 10:48:04 +09:00
Dick Choi
12453942c8 use _.forEach instead of Array.prototype.forEach.call 2016-10-26 10:47:51 +09:00
Dick Choi
b18a5be940 flowchart 2016-10-26 10:03:20 +09:00
Junyoung Choi
c2234747e8 Merge pull request #161 from twhiting/master
Updated readme english & reformatting
2016-10-25 09:49:18 +09:00
Trent
d6c9ab43ec Fixed spacing issue with markdown syntax 2016-10-24 17:29:09 -06:00
twhiting
db16a87f74 Formatting fixes for readme 2016-10-24 16:55:21 -06:00
twhiting
5b0d2ec97b Updated readme with english fixes + reformatting 2016-10-24 16:52:04 -06:00
Dick Choi
b906db3b24 fix syntax name bug of snippet note 2016-10-24 18:48:04 +09:00
Dick Choi
df0af2a11f Render codefence by codemirror rather than by hljs 2016-10-24 18:05:01 +09:00
Junyoung Choi
706dd3e616 Merge pull request #160 from tejado/master
Fix of storage unlink in preference popup
2016-10-22 16:50:47 +09:00
Dick Choi
bbec58e049 update contributors
add Mike Resoli and tjado
2016-10-21 16:59:37 +09:00
Dick Choi
381b7d85f4 keep the convention 2016-10-21 08:43:00 +09:00
Junyoung Choi
c17c056af3 Merge pull request #157 from tejado/master
Fix syntax error (unexpected token)
2016-10-21 08:13:18 +09:00
Junyoung Choi
a26a85cd1f Merge pull request #159 from mikeres0/patch-1
Improved error messaging
2016-10-21 02:50:48 +09:00
Mike Resoli
9b68b1d327 Improved error messaging
Instead of a standard `console.log` I've added an alert. Pressing the button and getting nowhere is not good from a user perspective.
2016-10-20 16:50:47 +01:00
tjado
6a6631052e Fix of storage unlink in preference popup 2016-10-20 13:35:02 +02:00
tjado
d4ad3a953a Fix syntax error (unexpected token) 2016-10-20 12:56:07 +02:00
Junyoung Choi
7bb63a78c5 Merge pull request #150 from mikeres0/master
Various updates
2016-10-20 00:53:30 +09:00
Mike Resoli
26c859f14c added npm run vendor to build.md 2016-10-19 14:41:01 +01:00
Mike Resoli
dd5c9bf3f6 Spelling mistake in index.js 2016-10-19 11:17:35 +01:00
Dick Choi
3db40fea31 v0.7.2 2016-10-18 14:46:17 +09:00
Dick Choi
8f1c198406 hot fix: Wrong behavior with Japanese IME keyboard 2016-10-18 13:42:10 +09:00
Dick Choi
ac65a3c86e v0.7.1 2016-10-18 00:32:28 +09:00
Dick Choi
79abcd90f6 fix tooltip layout of SideNav 2016-10-18 00:08:52 +09:00
Dick Choi
d614abdec6 fix wrong border color 2016-10-18 00:07:22 +09:00
Mike Resoli
44d754c59d Hid menu 2016-10-17 13:40:13 +01:00
Dick Choi
247d1db5d5 fix IME popup bug 2016-10-17 18:58:28 +09:00
Dick Choi
a52cfb2cd2 fix tag bug 2016-10-17 08:27:30 +09:00
Dick Choi
4e5d923388 Merged branch master into master 2016-10-17 08:12:03 +09:00
Dick Choi
f9598dd619 v0.7.0 2016-10-17 07:10:00 +09:00
Dick Choi
00f1c62646 fix typo 2016-10-17 07:01:52 +09:00
Dick Choi
8997728cbc add errorHandler for autoUpdater 2016-10-17 06:56:02 +09:00
Dick Choi
99a0704516 Merged branch master into v0.7.0 2016-10-17 06:38:32 +09:00
Dick Choi
434e361d5d fix bugs 2016-10-17 06:38:08 +09:00
Dick Choi
f112b0bf03 override meta.js 2016-10-17 02:16:28 +09:00
Dick Choi
0ce67ccd35 add codemirror addon 2016-10-17 02:15:40 +09:00
Dick Choi
c947feadeb hide help button(temp) 2016-10-17 02:15:14 +09:00
Dick Choi
2fa381980d use GFM as a default Markdown mode 2016-10-17 02:15:03 +09:00
Junyoung Choi
4e2c42f65c Merge pull request #144 from ryngonzalez/patch-1
Pateron => Patreon
2016-10-16 16:59:28 +09:00
Ryan Gonzalez
38b7d60af7 Pateron => Patreon
Correct misspelling of the crowd-funding platform's name.
2016-10-15 23:22:32 -07:00
Dick Choi
4714593d52 upgrade standard linter 2016-10-15 18:21:05 +09:00
Dick Choi
7729ed4f72 move statusbar 2016-10-15 18:20:13 +09:00
Dick Choi
7107777df3 fix NoteList bug in Finder 2016-10-15 00:20:56 +09:00
Dick Choi
53989b918e minor bug fix
use mime for syntax mode instead of mode
fix style of markdown
2016-10-15 00:20:22 +09:00
Dick Choi
fdd0c84441 update NoteDetail design 2016-10-14 04:00:59 +09:00
Dick Choi
8f0789bc6d renew SideNav
add contextmenu
fix MutableSet bug
2016-10-13 15:51:33 +09:00
Dick Choi
2fdea6cd61 fix minor bugs
fix codemirror bug
fix style bug of in NoteList
fix Redux store bug: Cruch on deleting empty folder
2016-10-13 15:47:25 +09:00
Dick Choi
ed5f3a6202 update style of list style button 2016-10-12 21:38:07 +09:00
Dick Choi
d98e909256 update design of SideNav
smaller menu
2016-10-12 21:31:12 +09:00
Dick Choi
6e0def310f update design of Note List
add list style
remove folder info
enhance design
2016-10-12 20:22:13 +09:00
Dick Choi
823da07a5e update storage handler for the future update 2016-10-12 12:48:53 +09:00
Dick Choi
40d2960562 add requseted features link 2016-10-12 12:48:10 +09:00
Dick Choi
6799f45352 Merged branch master into v0.7.0 2016-10-06 14:15:36 +09:00
Dick Choi
1209a044ce detect syntax by filename 2016-10-04 13:35:56 +09:00
Dick Choi
9a8760c120 delete ModeIcon & ModeSelect 2016-10-04 13:35:38 +09:00
Dick Choi
8d2a7716f3 add pateron link 2016-10-04 13:20:27 +09:00
Dick Choi
5f240ada59 apply devtron 2016-10-04 11:57:12 +09:00
Dick Choi
1c86dea4be indent setting 2016-10-04 11:09:27 +09:00
Dick Choi
92fd7ac09c use default theme if theme doesn't exist 2016-10-04 11:07:21 +09:00
Dick Choi
90b490c28b use codemirror 2016-10-03 22:28:13 +09:00
Dick Choi
8fd03a09de update readme.md 2016-09-29 21:45:26 +09:00
Dick Choi
041232fbdd Merge branch 'master'
Conflicts:
	oh-my-cdn.json
2016-09-29 20:59:49 +09:00
Dick Choi
f595121ab3 Merge pull request #129 from kg86/fix_and_improve_docs
Fix and improve docs
2016-09-24 16:10:12 +09:00
kg86
bd733312f4 add an instruction of npm install 2016-09-24 15:23:43 +09:00
kg86
70db2f78da add $ at the heading of each command 2016-09-24 15:17:38 +09:00
kg86
0298aba86f fix typos 2016-09-24 15:16:17 +09:00
Dick Choi
60b344eea8 Merge pull request #127 from MakeNowJust/patch-1
npmcdn is dead
2016-09-24 14:52:22 +09:00
TSUYUSATO Kitsune
eb63b743b1 use unpkg instead of npmcdn 2016-09-24 12:07:53 +09:00
Dick Choi
d09ffc9dd8 upgrade babel 2016-09-22 23:02:22 +09:00
Dick Choi
f1b2643941 apply new tray icon 2016-09-22 22:20:51 +09:00
Dick Choi
9e9d3ddc57 update resource images 2016-09-22 21:34:46 +09:00
Dick Choi
99a134494c upgrade katex 2016-09-22 20:28:12 +09:00
Dick Choi
d1156f963d upgrade electron(v1.3.6) 2016-09-22 20:15:38 +09:00
Dick Choi
2a6a2694d4 v0.6.8 2016-09-22 11:02:54 +09:00
Dick Choi
cc24e6b801 parse u2028 char properly 2016-09-22 11:01:23 +09:00
Dick Choi
9c85719270 v0.6.7 2016-09-21 16:49:49 +09:00
Dick Choi
394e86f765 fix quit bug on Ubuntu 2016-09-21 16:48:16 +09:00
Dick Choi
6bb9366ec8 fix update error 2016-09-21 16:00:18 +09:00
Dick Choi
8a9d4df6c7 add license attribute to package.json 2016-09-20 21:08:57 +09:00
Dick Choi
fdc2e91170 update screenshot 2016-09-20 21:01:17 +09:00
Dick Choi
40bf026e82 remove bottom margin of paragraph inside of list item 2016-09-20 21:01:11 +09:00
Dick Choi
975e82710e parse only cson files when reading notes 2016-09-20 21:00:49 +09:00
Dick Choi
1763dbcff1 discard LSEP 2016-09-20 21:00:29 +09:00
Dick Choi
908281356e v0.6.6 2016-09-20 16:54:03 +09:00
Dick Choi
123d5638cf collect count and version information 2016-09-20 16:28:46 +09:00
Dick Choi
79d0006f3f fix rename bug 2016-09-20 13:54:17 +09:00
Dick Choi
240b5daf3e attempt to recover invalid boostnote.json 2016-09-20 13:54:04 +09:00
Dick Choi
cd0bede2f2 add keepUnique method to mixpanel 2016-09-20 02:57:28 +09:00
Dick Choi
c5d984732a remove unnecessary script end tag 2016-09-20 02:56:55 +09:00
Dick Choi
7358e68394 fix update api bug 2016-09-15 08:43:53 +09:00
Dick Choi
27b09e5b73 fix mixpanel bug 2016-09-15 05:50:29 +09:00
Dick Choi
1705511b10 fix init bug 2016-09-15 05:31:39 +09:00
Dick Choi
e17fa6d0ed mixpanel 2016-09-15 05:12:15 +09:00
Dick Choi
9091976337 sort by updated At 2016-09-15 05:11:16 +09:00
Dick Choi
58d098503b add excluding syntax to SearchInput 2016-09-14 12:36:37 +09:00
Dick Choi
33fe4f5295 replace spaces with underscore of tag 2016-09-14 12:36:18 +09:00
Dick Choi
0646c4f8bd fix using wrong method name 2016-09-10 15:56:50 +09:00
Dick Choi
960469f07c bump up v0.6.5 2016-09-10 15:38:07 +09:00
Dick Choi
665f81ac9c apply new dataApi to initModal 2016-09-10 15:37:57 +09:00
Dick Choi
26f05b343e override default values to undefined attributes of config 2016-09-10 15:37:30 +09:00
Dick Choi
f50968f992 fix NoteList bug 2016-09-10 15:11:00 +09:00
Dick Choi
4c486c399b Merge pull request #110 from BoostIO/ui-improvement
Ui improvement for v0.6.5
2016-09-10 14:26:44 +09:00
Dick Choi
9f4dd909a8 right click to delete a note 2016-09-10 14:25:45 +09:00
Dick Choi
2b85aa1b88 default note type 2016-09-10 10:07:41 +09:00
Dick Choi
8e4c3a3b21 show confirmation dialog when deleting snippet with content 2016-09-09 14:37:35 +09:00
Dick Choi
a4160d2994 strip markdown syntax for title 2016-09-09 14:16:47 +09:00
Dick Choi
27e0252ccd resizable SideNav 2016-09-08 22:36:59 +09:00
Dick Choi
0a707b3f02 improve FolderSelect
maximum height and filtering by name
2016-09-08 22:02:07 +09:00
Dick Choi
6fc421810f fix bugs
Auto scroll method should not be called when selecting note out of list.
SearchInput bug
2016-09-08 22:01:27 +09:00
Dick Choi
22cf7443f4 Merge pull request #109 from BoostIO/data-api-refactor
use new api for finder
2016-09-08 19:12:55 +09:00
Dick Choi
8e7b4d2444 infinite scroll 2016-09-08 18:54:05 +09:00
Dick Choi
54437cec19 confirm on blur 2016-09-08 18:30:30 +09:00
Dick Choi
40fc63ea0c use new api for finder 2016-09-08 18:16:37 +09:00
Dick Choi
6bd81fe12a Merge pull request #100 from BoostIO/data-api-refactor
Data api refactoring
2016-09-08 16:50:43 +09:00
Dick Choi
519ea1a33f RENAME_STORAGE redux action 2016-09-08 16:50:05 +09:00
Dick Choi
f07f309393 ADD_STORAGE redux action 2016-09-08 16:46:18 +09:00
Dick Choi
7132e9ff24 REMOVE_STORAGE redux action
fix typo storageNoteMap
2016-09-08 16:45:47 +09:00
Dick Choi
34ae3cd704 use cheap-module-eval-source-map on developing 2016-09-08 16:32:17 +09:00
Dick Choi
fba972c98e remove deuplicate code 2016-09-02 09:41:01 +09:00
Dick Choi
a391ac682d UPDATE_FOLDER & DELETE_FOLDER
fix store bug when creating note
2016-09-02 09:33:01 +09:00
Dick Choi
4ee49d5991 lint colorpicker code 2016-09-01 00:49:25 +09:00
Dick Choi
0d573651a3 DELETE_NOTE 2016-09-01 00:02:16 +09:00
Dick Choi
52efc23984 fix navigate methods of note list 2016-09-01 00:02:09 +09:00
Dick Choi
aefb84df3b Mutable
INIT_ALL, NOTE_MOVE, NOTE_UPDATE(create/update) done
2016-09-01 00:01:47 +09:00
Dick Choi
ba374e08ff fix moveNote Api 2016-08-30 02:33:00 +09:00
Dick Choi
33a11ac2e5 integrate all methods 2016-08-29 10:12:14 +09:00
Dick Choi
357c4a382d migrateFromV5Storage 2016-08-29 10:05:56 +09:00
Dick Choi
d7e8f26ace rename transform method migrateFromV6Storage 2016-08-29 10:05:21 +09:00
Dick Choi
5c312c1939 moveNote 2016-08-28 21:54:38 +09:00
Dick Choi
5163ab134e fix text 2016-08-28 21:54:33 +09:00
Dick Choi
73dd0db529 deleteNote 2016-08-28 01:33:21 +09:00
Dick Choi
8921db89ab key should be labeled ~Key 2016-08-28 01:33:05 +09:00
Dick Choi
8d624459d4 add assertions to addStorage test
check version and folders attribute from resolved data and json
2016-08-28 00:20:08 +09:00
Dick Choi
ec96021b00 updateNote 2016-08-27 23:46:12 +09:00
Dick Choi
25f50b8cdf fix type
stoargeKey -> storageKey
2016-08-27 23:46:08 +09:00
Dick Choi
0127d5143a createNote 2016-08-27 22:13:32 +09:00
Dick Choi
ffe3b689c4 deleteFolder 2016-08-27 20:50:34 +09:00
Dick Choi
ff123be895 updateFolder 2016-08-27 17:40:37 +09:00
Dick Choi
8178ec5671 cleanup tests
remove mock-fs
update the comment of TestDummy.dummyStorage
change dummy storage name of createFolder test
2016-08-27 17:38:34 +09:00
Dick Choi
67dd089e67 createFolder 2016-08-27 16:31:45 +09:00
Dick Choi
8d96368ea6 cleanup test
remove crypto dependency from renameStorage
refactor removeStorage
remove dummy files
2016-08-27 16:05:29 +09:00
Dick Choi
eb163ef03c renew renameStorage api 2016-08-27 15:18:28 +09:00
Dick Choi
5558403358 renew addStorage 2016-08-27 15:02:00 +09:00
Dick Choi
db3a4d0f01 renew init method 2016-08-27 14:48:20 +09:00
Dick Choi
d01fe62757 Merge pull request #99 from yosmoc/export_codeeditor
multiple export default causes a error
2016-08-27 14:04:25 +09:00
Dick Choi
87cfc8f1de add fixtures.TestDummy and transform method 2016-08-27 14:00:25 +09:00
Dick Choi
3a8bef26d3 add fixtures.TestDummy and transform method 2016-08-27 12:23:58 +09:00
yosmoc
b550c0e9e3 multiple export default causes a error
only one export default allowed per module.
2016-08-27 02:25:39 +02:00
Dick Choi
8f1ee30553 extract createFolder method 2016-08-26 18:48:19 +09:00
Dick Choi
458174a5f5 add yosmoc to contributor list 2016-08-26 16:05:52 +09:00
Dick Choi
c5414aadd1 extract renameStorage, removeStorage methods
and tests for each of them
2016-08-26 10:30:10 +09:00
Dick Choi
eacd01e77e Merge pull request #98 from yosmoc/fix_typo
Fix typo
2016-08-26 09:25:14 +09:00
Dick Choi
089b919a68 Merge pull request #97 from yosmoc/node6
Node 6 support
2016-08-26 09:24:08 +09:00
yosmoc
2e5945642d fix typo 2016-08-25 21:33:07 +02:00
yosmoc
68a5b6fc50 works well with node6 2016-08-25 21:25:28 +02:00
Dick Choi
88538257ac extract addStorage api from dataApi and add its test
replace module-alias with babel-plugin-webpack-alias
2016-08-25 23:53:30 +09:00
Dick Choi
fb8041fb4b update dummy data 2016-08-25 17:05:05 +09:00
Dick Choi
e685c4302d add requirement node version 2016-08-25 16:32:48 +09:00
Dick Choi
103b56ea3d Merge pull request #96 from yosmoc/tab_tag_submit
submit the tag when receiving 'tab' key
2016-08-25 13:01:42 +09:00
yosmoc
0490b115ad submit the tag when receiving 'tab' key
In existing implementation, 'enter' key is only allowed to submit the tag. However, if the user finishes to create the tag and make another tag, the user naturally send 'tab' key.
When receiving 'tab' key, the focus will go to the star icon and nothing is submitted, but the text is still remained. This makes confusing to the user.

This commit provide 'tab' key submitting the tag also.
2016-08-24 22:34:34 +02:00
Dick Choi
5e1dd4a9ad Update readme.md 2016-08-24 11:07:55 +09:00
Dick Choi
93dd97a14a update readme 2016-08-24 10:48:42 +09:00
Dick Choi
52d065a38d init dataApi method test 2016-08-24 03:00:49 +09:00
Dick Choi
ad33e9f32d Merge branch 'master' of github.com:BoostIO/Boostnote 2016-08-23 14:46:47 +09:00
Dick Choi
b9f00c6971 bump up to v0.6.4 2016-08-23 23:43:04 +09:00
Dick Choi
dba5ea156a fix linux menu quit button 2016-08-23 23:41:05 +09:00
Dick Choi
631c86865f fix windows menu button 2016-08-23 14:29:04 +09:00
Dick Choi
324b8fc74a Merge branch 'master' of github.com:BoostIO/Boostnote 2016-08-23 01:52:42 +09:00
Dick Choi
282ca3ea2a move ipc server to main process 2016-08-23 01:51:30 +09:00
Dick Choi
17223db3ea move ipc server to main process 2016-08-23 00:52:33 +09:00
Dick Choi
cff3fdae6e refactor main-app process
clean unnecessary codes
2016-08-22 11:46:22 +09:00
Dick Choi
108e83a402 syntax mode will be detected by filename 2016-08-22 00:43:52 +09:00
Dick Choi
fc237848c8 fix Delete confirm design 2016-08-21 17:49:42 +09:00
Dick Choi
92b86bfa0b bump up electron v1.2.8 2016-08-20 23:54:15 +09:00
Dick Choi
b0fd17047c Merge branch 'master' of github.com:BoostIO/Boostnote 2016-08-14 17:44:15 +09:00
Dick Choi
bf8f82fbd8 Merge branch 'master' of github.com:BoostIO/Boostnote 2016-08-14 17:37:21 +09:00
Dick Choi
5468c60eaa v0.6.3 2016-08-14 17:31:13 +09:00
Dick Choi
8736d87b95 dark theme 2016-08-14 17:30:55 +09:00
Dick Choi
dd0440519b change home to all notes 2016-08-14 13:08:28 +09:00
Dick Choi
b7400553fc App on Linux without cinnamon must be terminated after closing main window 2016-08-14 04:15:27 +09:00
Dick Choi
9d2f570515 fix quit menu item of Linux menu 2016-08-14 12:21:31 +09:00
Dick Choi
7eeba0c082 App on Linux without cinnamon must be terminated after closing main window 2016-08-14 03:58:19 +09:00
Dick Choi
2e6bb21fda use global.process 2016-08-14 02:23:24 +09:00
Dick Choi
e30cfdf942 fix focusing bug 2016-08-14 02:20:57 +09:00
Dick Choi
b07fb92e5c fix Quit app shortcut on Windows 2016-08-14 02:07:08 +09:00
Dick Choi
5729125fbb update readme & package.json 2016-08-14 02:02:34 +09:00
Dick Choi
4577038abd fix package.json 2016-08-14 01:39:51 +09:00
Dick Choi
7649c1df9e Merge remote-tracking branch 'refs/remotes/origin/master'
Conflicts:
	package.json
2016-08-14 01:35:50 +09:00
Dick Choi
033bd111ad Merged finder into master 2016-08-14 01:35:19 +09:00
Dick Choi
2cbe07b373 Finder 2016-08-14 01:34:32 +09:00
Dick Choi
f6ec5c67a7 Merge pull request #77 from dotcs/feature-folder-color-picker
Feature: Folder color picker
2016-08-12 12:19:24 +09:00
Dick Choi
6bb78d3216 fix menu button tooltip style & set default font to buttons 2016-08-11 16:22:53 +09:00
Dick Choi
fa717a357d Merged branch master into master 2016-08-11 13:38:49 +09:00
Fabian Mueller
eed6bcc044 Set color picker pos correctly such that it is restricted to the viewport 2016-08-10 17:57:33 +02:00
Fabian Mueller
a01fd739bd Add color picker dependency; use color picker in storage dialog 2016-08-09 21:44:21 +02:00
Dick Choi
3b7ed5ffd7 foldable sidebar 2016-07-30 19:17:09 +09:00
Dick Choi
b3d9beea6d Merge pull request #71 from syossan27/fix-delete-last-note
Fix delete last note
2016-07-29 21:47:24 +09:00
syossan27
e256c7a7d9 Fix delete last note 2016-07-29 13:03:11 +09:00
Dick Choi
ef866f957a Markdown in snippet note will be rendered 2016-07-29 11:04:10 +09:00
Dick Choi
58d25415db fix layout bug of tab list in snippet note 2016-07-29 11:03:36 +09:00
Dick Choi
15f51a4064 modify contributing.md & clean unnecessary modules in gruntfile 2016-07-29 11:02:46 +09:00
Dick Choi
7fbe456e79 app will quit when main window close (win, linux only) 2016-07-28 17:41:46 +09:00
Dick Choi
9d8daac4cf v0.6.2 2016-07-28 15:26:07 +09:00
Dick Choi
ce199374d5 Merged branch finder into master 2016-07-28 14:52:24 +09:00
Dick Choi
4af8615624 set left border of tag item blue 2016-07-27 17:54:45 +09:00
Dick Choi
b6bb438507 fix checkbox rendering bug
it doesn't render multiline content properly
2016-07-27 17:54:16 +09:00
Dick Choi
49acd8a4f3 add tooltip 2016-07-27 14:13:54 +09:00
Dick Choi
3e699a99d5 fix preview bugs
autoscroll doesn't work at near end of note.
missing event handler
2016-07-27 14:13:42 +09:00
Dick Choi
e4238f9283 Merged branch dev into master 2016-07-26 20:12:47 +09:00
Dick Choi
9cd6d6d4c1 GFM checkbox 2016-07-26 20:00:32 +09:00
Dick Choi
49a4b5feb4 no more line anchors 2016-07-26 17:12:34 +09:00
Dick Choi
c6eff157de improve line anchors placement 2016-07-26 13:56:55 +09:00
Dick Choi
80d16233e7 foot note 2016-07-26 01:44:26 +09:00
Dick Choi
65e1a39027 debounce dispatch 2016-07-26 01:13:08 +09:00
Dick Choi
d73b567bd4 inhance UX & search case insensitive 2016-07-24 15:03:18 +09:00
Dick Choi
787bb0a9e6 update readme 2016-07-24 04:05:10 +09:00
186 changed files with 16694 additions and 5335 deletions

View File

@@ -3,6 +3,12 @@
"env": {
"development": {
"presets": ["react-hmre"]
},
"test": {
"presets": ["react", "es2015"],
"plugins": [
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
]
}
}
}

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
compiled/
dist/

10
.eslintrc Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": ["standard", "standard-jsx"],
"rules": {
"no-useless-escape": 0,
"prefer-const": "warn",
"no-unused-vars": "warn",
"no-undef": "warn",
"no-lone-blocks": "warn"
}
}

6
.travis.yml Normal file
View File

@@ -0,0 +1,6 @@
language: node_js
node_js:
- 'stable'
- 'lts/*'
script: npm run lint && npm run test

View File

@@ -1,6 +1,8 @@
Boostnote - the simplest note app
GPL-3.0
Copyright (C) 2016 MAISIN&CO.
Boostnote - an open source note-taking app made for programmers just like you.
Copyright (C) 2017 Maisin&Co., Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

View File

@@ -1,206 +1,159 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import path from 'path'
const ace = window.ace
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
this.changeHandler = (e) => this.handleChange(e)
this.blurHandler = (e) => {
e.stopPropagation()
this.blurHandler = (editor, e) => {
if (e == null) return null
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.root) {
isStillFocused = true
break
return
}
el = el.parentNode
}
if (!isStillFocused && this.props.onBlur != null) this.props.onBlur(e)
this.props.onBlur != null && this.props.onBlur(e)
}
this.killedBuffer = ''
this.execHandler = (e) => {
console.info('ACE COMMAND >> %s', e.command.name)
switch (e.command.name) {
case 'gotolinestart':
e.preventDefault()
{
let position = this.editor.getCursorPosition()
this.editor.navigateTo(position.row, 0)
}
break
case 'gotolineend':
e.preventDefault()
let position = this.editor.getCursorPosition()
this.editor.navigateTo(position.row, this.editor.getSession().getLine(position.row).length)
break
case 'jumptomatching':
e.preventDefault()
this.editor.navigateUp()
break
case 'removetolineend':
e.preventDefault()
let range = this.editor.getSelectionRange()
let session = this.editor.getSession()
if (range.isEmpty()) {
range.setEnd(range.start.row, session.getLine(range.start.row).length)
this.killedBuffer = session.getTextRange(range)
if (this.killedBuffer.length > 0) {
console.log('remove to lineend')
session.remove(range)
} else {
if (session.getLength() === range.start.row) {
return
}
range.setStart(range.start.row, range.end.col)
range.setEnd(range.start.row + 1, 0)
this.killedBuffer = '\n'
session.remove(range)
}
} else {
this.killedBuffer = session.getTextRange(range)
session.remove(range)
}
}
}
this.afterExecHandler = (e) => {
switch (e.command.name) {
case 'find':
Array.prototype.forEach.call(ReactDOM.findDOMNode(this).querySelectorAll('.ace_search_field, .ace_searchbtn, .ace_replacebtn, .ace_searchbtn_close'), (el) => {
el.removeEventListener('blur', this.blurHandler)
el.addEventListener('blur', this.blurHandler)
})
break
}
}
this.state = {
}
this.silentChange = false
}
componentWillReceiveProps (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
this.loadStyleHandler = (e) => {
this.editor.refresh()
}
}
componentDidMount () {
let { mode, value, theme, fontSize } = this.props
this.value = value
let el = ReactDOM.findDOMNode(this)
let editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/' + theme)
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(fontSize)
editor.on('blur', this.blurHandler)
editor.commands.addCommand({
name: 'Emacs cursor up',
bindKey: {mac: 'Ctrl-P'},
exec: function (editor) {
editor.navigateUp(1)
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
value: this.props.value,
lineNumbers: true,
lineWrapping: true,
theme: this.props.theme,
indentUnit: this.props.indentSize,
tabSize: this.props.indentSize,
indentWithTabs: this.props.indentType !== 'space',
keyMap: this.props.keyMap,
inputStyle: 'textarea',
dragDrop: false,
extraKeys: {
Tab: function (cm) {
const cursor = cm.getCursor()
const line = cm.getLine(cursor.line)
if (cm.somethingSelected()) cm.indentSelection('add')
else {
const tabs = cm.getOption('indentWithTabs')
if (line.trimLeft() === '- ' || line.trimLeft() === '* ' || line.trimLeft() === '+ ') {
cm.execCommand('goLineStart')
if (tabs) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
cm.execCommand('goLineEnd')
} else {
if (tabs) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
}
}
},
readOnly: true
})
editor.commands.addCommand({
name: 'Emacs kill buffer',
bindKey: {mac: 'Ctrl-Y'},
exec: function (editor) {
editor.insert(this.killedBuffer)
}.bind(this),
readOnly: true
'Cmd-T': function (cm) {
// Do nothing
},
Enter: 'newlineAndIndentContinueMarkdownList'
}
})
editor.commands.on('exec', this.execHandler)
editor.commands.on('afterExec', this.afterExecHandler)
this.setMode(this.props.mode)
var session = editor.getSession()
mode = _.find(modes, {name: mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
session.setUseSoftTabs(this.props.indentType === 'space')
session.setTabSize(this.props.indentSize)
session.setOption('useWorker', true)
session.setUseWrapMode(true)
session.setValue(_.isString(value) ? value : '')
session.on('change', this.changeHandler)
let editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler)
}
componentWillUnmount () {
this.editor.getSession().removeListener('change', this.changeHandler)
this.editor.removeListener('blur', this.blurHandler)
this.editor.commands.removeListener('exec', this.execHandler)
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
let editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler)
}
componentDidUpdate (prevProps, prevState) {
let { value } = this.props
this.value = value
let editor = this.editor
let session = this.editor.getSession()
let needRefresh = false
if (prevProps.mode !== this.props.mode) {
let mode = _.find(modes, {name: this.props.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
this.setMode(this.props.mode)
}
if (prevProps.theme !== this.props.theme) {
editor.setTheme('ace/theme/' + this.props.theme)
this.editor.setOption('theme', this.props.theme)
// editor should be refreshed after css loaded
}
if (prevProps.fontSize !== this.props.fontSize) {
editor.setFontSize(this.props.fontSize)
needRefresh = true
}
if (prevProps.fontFamily !== this.props.fontFamily) {
needRefresh = true
}
if (prevProps.keyMap !== this.props.keyMap) {
needRefresh = true
}
if (prevProps.indentSize !== this.props.indentSize) {
session.setTabSize(this.props.indentSize)
this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize)
}
if (prevProps.indentType !== this.props.indentType) {
session.setUseSoftTabs(this.props.indentType === 'space')
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
}
if (needRefresh) {
this.editor.refresh()
}
}
setMode (mode) {
let syntax = CodeMirror.findModeByName(pass(mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
this.editor.setOption('mode', syntax.mime)
CodeMirror.autoLoadMode(this.editor, syntax.mode)
}
handleChange (e) {
if (this.props.onChange) {
this.value = this.editor.getValue()
if (this.props.onChange) {
this.props.onChange(e)
}
}
getFirstVisibleRow () {
return this.editor.getFirstVisibleRow()
}
getCursorPosition () {
return this.editor.getCursorPosition()
}
moveCursorTo (row, col) {
this.editor.moveCursorTo(row, col)
}
scrollToLine (num) {
this.editor.scrollToLine(num, false, false)
}
focus () {
@@ -212,15 +165,36 @@ export default class CodeEditor extends React.Component {
}
reload () {
let session = this.editor.getSession()
session.removeListener('change', this.changeHandler)
session.setValue(this.props.value)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
// Change event shouldn't be fired when switch note
this.editor.off('change', this.changeHandler)
this.value = this.props.value
this.editor.setValue(this.props.value)
this.editor.clearHistory()
this.editor.on('change', this.changeHandler)
this.editor.refresh()
}
setValue (value) {
let cursor = this.editor.getCursor()
this.editor.setValue(value)
this.editor.setCursor(cursor)
}
handleDropImage (e) {
e.preventDefault()
const imagePath = e.dataTransfer.files[0].path
const filename = path.basename(imagePath)
const imageMd = `![${encodeURI(filename)}](${encodeURI(imagePath)})`
this.insertImage(imageMd)
}
insertImage (imageMd) {
const textarea = this.editor.getInputField()
textarea.value = textarea.value.substr(0, textarea.selectionStart) + imageMd + textarea.value.substr(textarea.selectionEnd)
}
render () {
let { className, fontFamily } = this.props
let { className, fontFamily, fontSize } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
@@ -233,8 +207,10 @@ export default class CodeEditor extends React.Component {
ref='root'
tabIndex='-1'
style={{
fontFamily: fontFamily.join(', ')
fontFamily: fontFamily.join(', '),
fontSize: fontSize
}}
onDrop={(e) => this.handleDropImage(e)}
/>
)
}
@@ -252,10 +228,9 @@ CodeEditor.propTypes = {
CodeEditor.defaultProps = {
readOnly: false,
theme: 'xcode',
keyMap: 'sublime',
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
}
export default CodeEditor

View File

@@ -3,24 +3,63 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownEditor.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
class MarkdownEditor extends React.Component {
constructor (props) {
super(props)
this.escapeFromEditor = ['Control', 'w']
this.supportMdSelectionBold = ['Control', ':']
this.state = {
status: 'PREVIEW'
status: 'PREVIEW',
renderValue: props.value,
keyPressed: {},
isLocked: false
}
this.lockEditorCode = () => this.handleLockEditor()
}
componentDidMount () {
this.value = this.refs.code.value
eventEmitter.on('editor:lock', this.lockEditorCode)
}
componentDidUpdate () {
this.value = this.refs.code.value
}
componentWillReceiveProps (props) {
if (props.value !== this.props.value) {
this.queueRendering(props.value)
}
}
componentWillUnmount () {
this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode)
}
queueRendering (value) {
clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => {
this.renderPreview(value)
}, 500)
}
cancelQueue () {
clearTimeout(this.renderTimer)
}
renderPreview (value) {
this.setState({
renderValue: value
})
}
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
@@ -41,20 +80,24 @@ class MarkdownEditor extends React.Component {
this.refs.code.blur()
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
})
}
}
handleBlur (e) {
if (this.state.isLocked) return
this.setState({ keyPressed: [] })
let { config } = this.props
if (config.editor.switchPreview === 'BLUR') {
let cursorPosition = this.refs.code.getCursorPosition()
let cursorPosition = this.refs.code.editor.getCursor()
this.setState({
status: 'PREVIEW'
}, () => {
this.refs.preview.focus()
this.refs.preview.scrollTo(cursorPosition.row)
this.refs.preview.scrollTo(cursorPosition.line)
})
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
}
@@ -70,6 +113,30 @@ class MarkdownEditor extends React.Component {
}, () => {
this.refs.code.focus()
})
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
}
handleCheckboxClick (e) {
e.preventDefault()
e.stopPropagation()
let idMatch = /checkbox-([0-9]+)/
let checkedMatch = /\[x\]/i
let uncheckedMatch = /\[ \]/
if (idMatch.test(e.target.getAttribute('id'))) {
let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
let lines = this.refs.code.value
.split('\n')
let targetLine = lines[lineIndex]
if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
}
}
@@ -83,14 +150,59 @@ class MarkdownEditor extends React.Component {
} else {
this.refs.code.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
reload () {
this.refs.code.reload()
this.cancelQueue()
this.renderPreview(this.props.value)
}
handleKeyDown (e) {
if (this.state.status !== 'CODE') return false
const keyPressed = Object.assign(this.state.keyPressed, {
[e.key]: true
})
this.setState({ keyPressed })
let isNoteHandlerKey = (el) => { return this.state.keyPressed[el] }
if (!this.state.isLocked && this.state.status === 'CODE' && this.escapeFromEditor.every(isNoteHandlerKey)) {
document.activeElement.blur()
}
if (this.supportMdSelectionBold.every(isNoteHandlerKey)) {
this.addMdAroundWord('**')
}
}
addMdAroundWord (mdElement) {
if (this.refs.code.editor.getSelection()) {
return this.addMdAroundSelection(mdElement)
}
const currentCaret = this.refs.code.editor.getCursor()
const word = this.refs.code.editor.findWordAt(currentCaret)
const cmDoc = this.refs.code.editor.getDoc()
cmDoc.replaceRange(mdElement, word.anchor)
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length })
}
addMdAroundSelection (mdElement) {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
}
handleKeyUp (e) {
const keyPressed = Object.assign(this.state.keyPressed, {
[e.key]: false
})
this.setState({ keyPressed })
}
handleLockEditor () {
this.setState({ isLocked: !this.state.isLocked })
}
render () {
let { className, value, config } = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
@@ -106,12 +218,15 @@ class MarkdownEditor extends React.Component {
}
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
>
<CodeEditor styleName='codeEditor'
ref='code'
mode='markdown'
mode='GitHub Flavored Markdown'
value={value}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
@@ -124,17 +239,21 @@ class MarkdownEditor extends React.Component {
: 'preview--hide'
}
style={previewStyle}
theme={config.ui.theme}
keyMap={config.editor.keyMap}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
tabIndex='0'
value={value}
value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
/>
</div>
)

View File

@@ -1,16 +1,82 @@
import React, { PropTypes } from 'react'
import markdown from 'browser/lib/markdown'
import _ from 'lodash'
import hljsTheme from 'browser/lib/hljsThemes'
import CodeMirror from 'codemirror'
import consts from 'browser/lib/consts'
import Raphael from 'raphael'
import flowchart from 'flowchart'
import SequenceDiagram from 'js-sequence-diagrams'
import eventEmitter from 'browser/main/lib/eventEmitter'
import fs from 'fs'
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
const goExternal = function (e) {
e.preventDefault()
e.stopPropagation()
shell.openExternal(e.target.href)
function decodeHTMLEntities (text) {
var entities = [
['apos', '\''],
['amp', '&'],
['lt', '<'],
['gt', '>']
]
for (var i = 0, max = entities.length; i < max; ++i) {
text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1])
}
return text
}
const { remote } = require('electron')
const { app } = remote
const path = require('path')
const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
? app.getAppPath()
: path.resolve())
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
return `
@font-face {
font-family: 'Lato';
src: url('${appPath}/resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('${appPath}/resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
${markdownStyle}
body {
font-family: ${fontFamily.join(', ')};
font-size: ${fontSize}px;
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
background-color: rgba(0,0,0,0.04);
color: #CC305F;
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
}
h1, h2 {
border: none;
}
h1 {
padding-bottom: 4px;
margin: 1em 0 8px;
}
h2 {
padding-bottom: 0.2em;
margin: 1em 0 0.37em;
}
`
}
const { shell } = require('electron')
const OSX = global.process.platform === 'darwin'
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
@@ -27,6 +93,30 @@ export default class MarkdownPreview extends React.Component {
this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e)
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
}
handlePreviewAnchorClick (e) {
e.preventDefault()
e.stopPropagation()
let anchor = e.target.closest('a')
let href = anchor.getAttribute('href')
if (_.isString(href) && href.match(/^#/)) {
let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
}
} else {
shell.openExternal(href)
}
}
handleCheckboxClick (e) {
this.props.onCheckboxClick(e)
}
handleContextMenu (e) {
@@ -34,84 +124,190 @@ export default class MarkdownPreview extends React.Component {
}
handleMouseDown (e) {
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
case 'INPUT':
return null
}
}
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
}
handleMouseUp (e) {
if (e.target != null && e.target.tagName === 'A') {
return null
}
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
handleSaveAsText () {
this.exportAsDocument('txt')
}
handleSaveAsMd () {
this.exportAsDocument('md')
}
exportAsDocument (fileType) {
const options = {
filters: [
{ name: 'Documents', extensions: [fileType] }
],
properties: ['openFile', 'createDirectory']
}
dialog.showSaveDialog(remote.getCurrentWindow(), options,
(filename) => {
if (filename) {
fs.writeFile(filename, this.props.value, (err) => {
if (err) throw err
})
}
})
}
componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.head.innerHTML = `
<style id='style'></style>
<link rel="stylesheet" href="${appPath}/node_modules/katex/dist/katex.min.css">
<link rel="stylesheet" href="${appPath}/node_modules/codemirror/lib/codemirror.css">
<link rel="stylesheet" id="codeTheme">
`
this.rewriteIframe()
this.applyStyle()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
}
componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value ||
prevProps.fontFamily !== this.props.fontFamily ||
if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber
) this.rewriteIframe()
prevProps.lineNumber !== this.props.lineNumber ||
prevProps.theme !== this.props.theme) {
this.applyStyle()
this.rewriteIframe()
}
}
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', goExternal)
})
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
applyStyle () {
let { fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? [fontFamily].concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode'
this.refs.root.contentWindow.document.head.innerHTML = `
<style>
@font-face {
font-family: 'Lato';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
this.setCodeTheme(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
}
${markdownStyle}
body {
font-family: ${fontFamily.join(', ')};
font-size: ${fontSize}px;
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
opacity: 0.5;
}
</style>
<link rel="stylesheet" href="../node_modules/highlight.js/styles/${codeBlockTheme}.css">
<link rel="stylesheet" href="../resources/katex.min.css">
`
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('mousedown', goExternal)
setCodeTheme (theme) {
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
? theme
: 'elegant'
this.getWindow().document.getElementById('codeTheme').href = `${appPath}/node_modules/codemirror/theme/${theme}.css`
}
rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.removeEventListener('click', this.checkboxClickHandler)
})
let { value, theme, indentSize, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
el.parentNode.parentNode.style.listStyleType = 'none'
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.addEventListener('click', this.checkboxClickHandler)
})
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
let syntax = CodeMirror.findModeByName(el.className)
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
CodeMirror.requireMode(syntax.mode, () => {
let content = decodeHTMLEntities(el.innerHTML)
el.innerHTML = ''
el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
})
})
})
let opts = {}
// if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD'
// opts['line-color'] = '#DDD'
// opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C'
// }
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
Raphael.setWindow(this.getWindow())
try {
let diagram = flowchart.parse(decodeHTMLEntities(el.innerHTML))
el.innerHTML = ''
diagram.drawSVG(el, opts)
_.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
})
} catch (e) {
console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
Raphael.setWindow(this.getWindow())
try {
let diagram = SequenceDiagram.parse(decodeHTMLEntities(el.innerHTML))
el.innerHTML = ''
diagram.drawSVG(el, {theme: 'simple'})
_.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
})
} catch (e) {
console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
})
}
@@ -124,19 +320,24 @@ export default class MarkdownPreview extends React.Component {
}
scrollTo (targetRow) {
let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor')
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
for (let index = 0; index < lineAnchors.length; index++) {
let lineAnchor = lineAnchors[index]
let row = parseInt(lineAnchor.getAttribute('data-key'))
if (row > targetRow) {
let targetAnchor = lineAnchors[index - 1]
this.getWindow().scrollTo(0, targetAnchor.offsetTop)
for (let index = 0; index < blocks.length; index++) {
let block = blocks[index]
let row = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
block = blocks[index - 1]
block != null && this.getWindow().scrollTo(0, block.offsetTop)
break
}
}
}
preventImageDroppedHandler (e) {
e.preventDefault()
e.stopPropagation()
}
render () {
let { className, style, tabIndex } = this.props
return (
@@ -157,7 +358,6 @@ MarkdownPreview.propTypes = {
onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
value: PropTypes.string
}

View File

@@ -1,82 +0,0 @@
import React, { PropTypes } from 'react'
export default class ModeIcon extends React.Component {
getClassName () {
var mode = this.props.mode
switch (mode) {
// Script
case 'javascript':
return 'devicon-javascript-plain'
case 'jsx':
return 'devicon-react-original'
case 'coffee':
return 'devicon-coffeescript-original'
case 'ruby':
return 'devicon-ruby-plain'
case 'erlang':
return 'devicon-erlang-plain'
case 'php':
return 'devicon-php-plain'
// HTML
case 'html':
return 'devicon-html5-plain'
// Stylesheet
case 'css':
return 'devicon-css3-plain'
case 'less':
return 'devicon-less-plain-wordmark'
case 'sass':
case 'scss':
return 'devicon-sass-original'
// Compile
case 'c':
return 'devicon-c-plain'
case 'cpp':
return 'devicon-cplusplus-plain'
case 'csharp':
return 'devicon-csharp-plain'
case 'objc':
return 'devicon-apple-original'
case 'golang':
return 'devicon-go-plain'
case 'java':
return 'devicon-java-plain'
// Framework
case 'django':
return 'devicon-django-plain'
// Config
case 'dockerfile':
return 'devicon-docker-plain'
case 'gitignore':
return 'devicon-git-plain'
// Shell
case 'sh':
case 'batchfile':
case 'powershell':
return 'fa fa-fw fa-terminal'
case 'text':
case 'markdown':
return 'fa fa-fw fa-file-text-o'
}
return 'fa fa-fw fa-code'
}
render () {
let className = `ModeIcon ${this.getClassName()} ${this.props.className}`
return (
<i className={className}/>
)
}
}
ModeIcon.propTypes = {
className: PropTypes.string,
mode: PropTypes.string
}

View File

@@ -1,172 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from './ModeIcon'
import modes from '../lib/modes'
import _ from 'lodash'
const IDLE_MODE = 'IDLE_MODE'
const EDIT_MODE = 'EDIT_MODE'
export default class ModeSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
mode: IDLE_MODE,
search: '',
focusIndex: 0
}
}
componentDidMount () {
this.blurHandler = e => {
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (this.state.mode === EDIT_MODE && document.activeElement !== searchElement) {
this.handleBlur(e)
}
}
window.addEventListener('click', this.blurHandler)
}
componentWillUnmount () {
window.removeEventListener('click', this.blurHandler)
let searchElement = ReactDOM.findDOMNode(this.refs.search)
if (searchElement != null && this.searchKeyDownListener != null) {
searchElement.removeEventListener('keydown', this.searchKeyDownListener)
}
}
handleIdleSelectClick (e) {
this.setState({mode: EDIT_MODE, search: this.props.value}, () => {
ReactDOM.findDOMNode(this.refs.search).select()
})
}
handleModeOptionClick (modeName) {
return e => {
this.props.onChange(modeName)
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
}
handleSearchKeyDown (e) {
switch (e.keyCode) {
// up
case 38:
e.preventDefault()
if (this.state.focusIndex > 0) this.setState({focusIndex: this.state.focusIndex - 1})
break
// down
case 40:
e.preventDefault()
{
let search = _.escapeRegExp(this.state.search)
let filteredModes = modes
.filter(mode => {
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
if (filteredModes.length === this.state.focusIndex + 1) this.setState({focusIndex: filteredModes.length - 1})
else this.setState({focusIndex: this.state.focusIndex + 1})
}
break
// enter
case 13:
e.preventDefault()
{
let search = _.escapeRegExp(this.state.search)
let filteredModes = modes
.filter(mode => {
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
let targetMode = filteredModes[this.state.focusIndex]
if (targetMode != null) {
this.props.onChange(targetMode.name)
this.setIdle()
}
}
break
// esc
case 27:
case 9:
e.stopPropagation()
this.setIdle()
}
if (this.props.onKeyDown) this.props.onKeyDown(e)
}
handleSearchChange (e) {
this.setState({
search: e.target.value,
focusIndex: 0
})
}
handleBlur (e) {
if (e.target !== ReactDOM.findDOMNode(this.refs.search)) {
this.setIdle()
}
}
setIdle () {
this.setState({
mode: IDLE_MODE,
search: '',
focusIndex: 0
})
}
render () {
let className = this.props.className != null
? `ModeSelect ${this.props.className}`
: this.props.className
if (this.state.mode === IDLE_MODE) {
let mode = _.findWhere(modes, {name: this.props.value})
let modeName = mode != null ? mode.name : 'text'
let modeLabel = mode != null ? mode.label : this.props.value
return (
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
<ModeIcon mode={modeName}/>{modeLabel}
</div>
)
}
let search = _.escapeRegExp(this.state.search)
let filteredOptions = modes
.filter(mode => {
let nameMatched = mode.name.match(search)
let aliasMatched = _.some(mode.alias, alias => alias.match(search))
return nameMatched || aliasMatched
})
.map((mode, index) => {
return (
<div key={mode.name} className={index === this.state.focusIndex ? 'ModeSelect-options-item active' : 'ModeSelect-options-item'} onClick={e => this.handleModeOptionClick(mode.name)(e)}><ModeIcon mode={mode.name}/>{mode.label}</div>
)
})
return (
<div className={className + ' edit'}>
<input onBlur={e => this.handleBlur(e)} onKeyDown={e => this.handleSearchKeyDown(e)} ref='search' onChange={e => this.handleSearchChange(e)} value={this.state.search} type='text'/>
<div ref='options' className='ModeSelect-options hide'>
{filteredOptions}
</div>
</div>
)
}
}
ModeSelect.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
onChange: PropTypes.func,
onKeyDown: PropTypes.func
}

View File

@@ -0,0 +1,97 @@
/**
* @fileoverview Note item component.
*/
import React, { PropTypes } from 'react'
import { isArray } from 'lodash'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteItem.styl'
/**
* @description Tag element component.
* @param {string} tagName
* @return {React.Component}
*/
const TagElement = ({ tagName }) => (
<span styleName='item-bottom-tagList-item' key={tagName}>
#{tagName}
</span>
)
/**
* @description Tag element list component.
* @param {Array|null} tags
* @return {React.Component}
*/
const TagElementList = (tags) => {
if (!isArray(tags)) {
return []
}
const tagElements = tags.map(tag => (
TagElement({tagName: tag})
))
return tagElements
}
/**
* @description Note item component when using normal display mode.
* @param {boolean} isActive
* @param {Object} note
* @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu
* @param {string} dateDisplay
*/
const NoteItem = ({ isActive, note, dateDisplay, handleNoteClick, handleNoteContextMenu }) => (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
>
<div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
}
<div styleName='item-title'>
{note.title.trim().length > 0
? note.title
: <span styleName='item-title-empty'>Empty</span>
}
</div>
<div styleName='item-bottom-time'>{dateDisplay}</div>
{note.isStarred
? <i styleName='item-star' className='fa fa-star' /> : ''
}
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags)
: <span styleName='item-bottom-tagList-empty' />
}
</div>
</div>
</div>
</div>
)
NoteItem.propTypes = {
isActive: PropTypes.bool.isRequired,
dateDisplay: PropTypes.string.isRequired,
note: PropTypes.shape({
storage: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
title: PropTypes.string.isrequired,
tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired
}),
handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired
}
export default CSSModules(NoteItem, styles)

View File

@@ -0,0 +1,187 @@
$control-height = 30px
.root
absolute left bottom
top $topBar-height - 1
background-color $ui-noteList-backgroundColor
.item
position relative
padding 0 20px
user-select none
cursor pointer
background-color $ui-noteList-backgroundColor
transition background-color 0.2s
&:hover
background-color alpha($ui-button--active-backgroundColor, 40%)
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-text-color
.item-bottom-tagList-item
background-color alpha(white, 0.6)
color $ui-text-color
&:active
background-color $ui-button--active-backgroundColor
color $ui-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-text-color
.item-bottom-tagList-item
background-color alpha(white, 0.6)
color $ui-text-color
.item-wrapper
padding 15px 0
border-bottom $ui-border
.item--active
@extend .item
background-color $ui-button--active-backgroundColor
color $ui-text-color
.item-title
.item-title-empty
.item-bottom-tagList-empty
.item-bottom-time
.item-title-icon
color $ui-text-color
.item-bottom-tagList-item
background-color alpha(white, 0.6)
color $ui-text-color
.item-wrapper
border-color transparent
&:hover
background-color $ui-button--active-backgroundColor
.item-title-icon
position relative
font-size 12px
color $ui-inactive-text-color
.item-title
font-size 13px
position relative
top -12px
left 20px
padding-right 15px
padding-bottom 4px
overflow ellipsis
color $ui-inactive-text-color
.item-title-empty
font-weight normal
color $ui-inactive-text-color
.item-bottom
position relative
bottom 0px
margin-top 2px
height 20px
font-size 12px
line-height 20px
overflow ellipsis
display flex
.item-bottom-tagList
flex 1
overflow ellipsis
line-height 20px
padding-left 2px
.item-bottom-tagList-item
font-size 10px
margin-right 8px
padding 0
height 20px
box-sizing border-box
border-radius 2px
padding 1px 2px
vertical-align middle
background-color white
color $ui-inactive-text-color
.item-bottom-time
color $ui-inactive-text-color
font-size 10px
padding-left 2px
padding-bottom 2px
.item-star
position absolute
top 19px
left 5px
width 34px
height 34px
color $ui-favorite-star-button-color
font-size 12px
padding 0
border-radius 17px
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
.item
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
&:hover
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dark-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-dark-text-color
&:active
transition 0.15s
background-color $ui-dark-button--active-backgroundColor
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dark-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha(white, 10%)
color $ui-dark-text-color
.item-wrapper
border-color $ui-dark-borderColor
.item--active
border-color $ui-dark-borderColor
background-color $ui-dark-button--active-backgroundColor
.item-wrapper
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color $ui-dark-text-color
.item-bottom-tagList-item
background-color alpha(white, 10%)
color $ui-dark-text-color
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -0,0 +1,49 @@
/**
* @fileoverview Note item component with simple display mode.
*/
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteItemSimple.styl'
/**
* @description Note item component when using simple display mode.
* @param {boolean} isActive
* @param {Object} note
* @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu
*/
const NoteItemSimple = ({ isActive, note, handleNoteClick, handleNoteContextMenu }) => (
<div styleName={isActive
? 'item-simple--active'
: 'item-simple'
}
key={`${note.storage}-${note.key}`}
onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)}
onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)}
>
<div styleName='item-simple-title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' />
}
{note.title.trim().length > 0
? note.title
: <span styleName='item-simple-title-empty'>Empty</span>
}
</div>
</div>
)
NoteItemSimple.propTypes = {
isActive: PropTypes.bool.isRequired,
note: PropTypes.shape({
storage: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
title: PropTypes.string.isrequired
}),
handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired
}
export default CSSModules(NoteItemSimple, styles)

View File

@@ -0,0 +1,105 @@
$control-height = 30px
.root
absolute left bottom
top $topBar-height - 1
background-color $ui-noteList-backgroundColor
.item-simple
position relative
padding 0 25px
user-select none
cursor pointer
background-color $ui-noteList-backgroundColor
transition background-color 0.15s
&:hover
background-color alpha($ui-button--active-backgroundColor, 40%)
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
color $ui-text-color
&:active
background-color $ui-button--active-backgroundColor
color $ui-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
color $ui-text-color
.item-simple--active
@extend .item-simple
background-color $ui-button--active-backgroundColor
color $ui-text-color
.item-simple-title
.item-simple-title-empty
border-color transparent
color $ui-text-color
.item-simple-title-icon
color $ui-text-color
&:hover
background-color $ui-button--active-backgroundColor
.item-simple-title
font-size 12px
height 40px
box-sizing border-box
line-height 24px
padding-top 8px
overflow ellipsis
color $ui-inactive-text-color
border-bottom $ui-border
.item-simple-title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 6px
.item-simple-title-empty
font-weight normal
color $ui-inactive-text-color
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
.item-simple
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
&:active
background-color $ui-dark-button--active-backgroundColor
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
.item-simple-bottom-tagList-item
transition 0.15s
color $ui-dark-text-color
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
.item-simple-bottom-tagList-item
transition 0.15s
color $ui-dark-text-color
.item-simple--active
border-color $ui-dark-borderColor
background-color $ui-dark-button--active-backgroundColor
.item-simple-title
.item-simple-title-icon
.item-simple-bottom-time
color $ui-dark-text-color
.item-simple-bottom-tagList-item
background-color transparent
color $ui-dark-text-color
.item-simple-title
color $ui-inactive-text-color
border-color $ui-dark-borderColor
.item-simple-title-icon
color $ui-darkinactive-text-color
.item-simple-title-empty
color $ui-dark-inactive-text-color

View File

@@ -0,0 +1,44 @@
/**
* @fileoverview Filter for all notes.
*/
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SideNavFilter.styl'
/**
* @param {boolean} isFolded
* @param {boolean} isHomeActive
* @param {Function} handleAllNotesButtonClick
* @param {boolean} isStarredActive
* @param {Function} handleStarredButtonClick
* @return {React.Component}
*/
const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick,
isStarredActive, handleStarredButtonClick
}) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={handleAllNotesButtonClick}
>
<i className='fa fa-archive fa-fw' />
<span styleName='menu-button-label'>All Notes</span>
</button>
<button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
onClick={handleStarredButtonClick}
>
<i className='fa fa-star fa-fw' />
<span styleName='menu-button-label'>Starred</span>
</button>
</div>
)
SideNavFilter.propTypes = {
isFolded: PropTypes.bool,
isHomeActive: PropTypes.bool.isRequired,
handleAllNotesButtonClick: PropTypes.func.isRequired,
isStarredActive: PropTypes.bool.isRequired,
handleStarredButtonClick: PropTypes.func.isRequired
}
export default CSSModules(SideNavFilter, styles)

View File

@@ -0,0 +1,105 @@
.menu
margin-bottom 30px
.menu-button
navButtonColor()
height 32px
padding 0 15px
font-size 12px
width 100%
text-align left
overflow ellipsis
.menu-button--active
@extend .menu-button
color #e74c3c
background-color $ui-button--active-backgroundColor
.menu-button-label
color $ui-text-color
&:hover
background-color $ui-button--active-backgroundColor
color #e74c3c
.menu-button-label
color $ui-text-color
&:active, &:active:hover
background-color $ui-button--active-backgroundColor
color #e74c3c
.menu-button-label
color $ui-text-color
.menu-button-star--active
@extend .menu-button
color #F9BF3B
background-color $ui-button--active-backgroundColor
.menu-button-label
color $ui-text-color
&:hover
background-color $ui-button--active-backgroundColor
color #F9BF3B
.menu-button-label
color $ui-text-color
&:active, &:active:hover
background-color $ui-button--active-backgroundColor
color #F9BF3B
.menu-button-label
color $ui-text-color
.menu-button-label
margin-left 5px
.menu--folded
@extend .menu
.menu-button, .menu-button--active
text-align center
&:hover .menu-button-label
transition opacity 0.15s
opacity 1
.menu-button-label
position fixed
display inline-block
height 32px
left 44px
padding 0 10px
margin-top -8px
margin-left 0
overflow ellipsis
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 32px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
opacity 0
font-size 12px
body[data-theme="dark"]
.menu-button
&:active
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color
.menu-button--active
color #c0392b
background-color $ui-dark-button--active-backgroundColor
.menu-button-label
color $ui-dark-text-color
&:hover
background-color $ui-dark-button--active-backgroundColor
color #c0392b
.menu-button-label
color $ui-dark-text-color
.menu-button-star--active
color $ui-favorite-star-button-color
background-color $ui-dark-button--active-backgroundColor
.menu-button-label
color $ui-dark-text-color
&:hover
background-color $ui-dark-button--active-backgroundColor
color $ui-favorite-star-button-color
.menu-button-label
color $ui-dark-text-color

View File

@@ -0,0 +1,132 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl'
import context from 'browser/lib/context'
class SnippetTab extends React.Component {
constructor (props) {
super(props)
this.state = {
isRenaming: false,
name: props.snippet.name
}
}
componentWillUpdate (nextProps) {
if (nextProps.snippet.name !== this.props.snippet.name) {
this.setState({
name: nextProps.snippet.name
})
}
}
handleClick (e) {
this.props.onClick(e)
}
handleContextMenu (e) {
context.popup([
{
label: 'Rename',
click: (e) => this.handleRenameClick(e)
}
])
}
handleRenameClick (e) {
this.startRenaming()
}
handleNameInputBlur (e) {
this.handleRename()
}
handleNameInputChange (e) {
this.setState({
name: e.target.value
})
}
handleNameInputKeyDown (e) {
switch (e.keyCode) {
case 13:
this.handleRename()
break
case 27:
this.setState({
name: this.props.snippet.name,
isRenaming: false
})
break
}
}
handleRename () {
this.setState({
isRenaming: false
}, () => {
if (this.props.snippet.name !== this.state.name) {
this.props.onRename(this.state.name)
}
})
}
handleDeleteButtonClick (e) {
this.props.onDelete(e)
}
startRenaming () {
this.setState({
isRenaming: true
}, () => {
this.refs.name.focus()
this.refs.name.select()
})
}
render () {
let { isActive, snippet, isDeletable } = this.props
return (
<div styleName={isActive
? 'root--active'
: 'root'
}
>
{!this.state.isRenaming
? <button styleName='button'
onClick={(e) => this.handleClick(e)}
onDoubleClick={(e) => this.handleRenameClick(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='button-unnamed'>
Unnamed
</span>
}
</button>
: <input styleName='input'
ref='name'
value={this.state.name}
onChange={(e) => this.handleNameInputChange(e)}
onBlur={(e) => this.handleNameInputBlur(e)}
onKeyDown={(e) => this.handleNameInputKeyDown(e)}
/>
}
{isDeletable &&
<button styleName='deleteButton'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
<i className='fa fa-times' />
</button>
}
</div>
)
}
}
SnippetTab.propTypes = {
}
export default CSSModules(SnippetTab, styles)

View File

@@ -0,0 +1,91 @@
.root
position relative
flex 1
overflow hidden
&:hover
.deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
.root--active
@extend .root
min-width 100px
border-bottom $ui-border
.button
width 100%
height 29px
overflow ellipsis
text-align left
padding-right 30px
border none
background-color transparent
transition 0.15s
border-left 4px solid transparent
&:hover
background-color $ui-button--hover-backgroundColor
.deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.input
height 29px
border $ui-active-color
padding 0 5px
width 100%
outline none
body[data-theme="dark"]
.root
color $ui-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.deleteButton
color $ui-dark-inactive-text-color
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.deleteButton
color $ui-dark-inactive-text-color
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-dark-text-color
background-color $ui-dark-button--hover-backgroundColor
.input
background-color $ui-dark-button--hover-backgroundColor
color $ui-dark-text-color

View File

@@ -0,0 +1,58 @@
/**
* @fileoverview Micro component for showing storage.
*/
import React, { PropTypes } from 'react'
import styles from './StorageItem.styl'
import CSSModules from 'browser/lib/CSSModules'
import { isNumber } from 'lodash'
/**
* @param {boolean} isActive
* @param {Function} handleButtonClick
* @param {Function} handleContextMenu
* @param {string} folderName
* @param {string} folderColor
* @param {boolean} isFolded
* @param {number} noteCount
* @return {React.Component}
*/
const StorageItem = ({
isActive, handleButtonClick, handleContextMenu, folderName,
folderColor, isFolded, noteCount
}) => (
<button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
onClick={handleButtonClick}
onContextMenu={handleContextMenu}
>
<span styleName={isFolded
? 'folderList-item-name--folded' : 'folderList-item-name'
}
style={{borderColor: folderColor}}
>
{isFolded ? folderName.substring(0, 1) : folderName}
</span>
{(!isFolded && isNumber(noteCount)) &&
<span styleName='folderList-item-noteCount'>{noteCount}</span>
}
{isFolded &&
<span styleName='folderList-item-tooltip'>
{folderName}
</span>
}
</button>
)
StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired,
handleButtonClick: PropTypes.func,
handleContextMenu: PropTypes.func,
folderName: PropTypes.string.isRequired,
folderColor: PropTypes.string,
isFolded: PropTypes.bool.isRequired,
noteCount: PropTypes.number
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,92 @@
.root
width 100%
user-select none
.folderList-item
display flex
width 100%
height 26px
background-color transparent
color $ui-inactive-text-color
padding 0
margin-bottom 5px
text-align left
border none
overflow ellipsis
font-size 12px
&:first-child
margin-top 0
&:hover
color $ui-text-color
background-color alpha($ui-button--active-backgroundColor, 20%)
transition background-color 0.15s
&:active
color $ui-text-color
background-color $ui-button--active-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-text-color
background-color $ui-button--active-backgroundColor
&:hover
color $ui-text-color
background-color $ui-button--active-backgroundColor
.folderList-item-name
display block
flex 1
padding 0 15px
height 26px
line-height 26px
border-width 0 0 0 2px
border-style solid
border-color transparent
overflow hidden
text-overflow ellipsis
.folderList-item-noteCount
float right
line-height 26px
padding-right 15px
font-size 12px
.folderList-item-tooltip
tooltip()
position fixed
padding 0 10px
left 44px
z-index 10
pointer-events none
opacity 0
border-top-right-radius 2px
border-bottom-right-radius 2px
height 26px
line-height 26px
.folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip
opacity 1
.folderList-item-name--folded
@extend .folderList-item-name
padding-left 12px
body[data-theme="dark"]
.folderList-item
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
&:active
background-color $ui-dark-button--active-backgroundColor
&:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor

View File

@@ -54,7 +54,7 @@ body
font-family helvetica, arial, sans-serif
line-height 1.6
overflow-x hidden
user-select all
background-color $ui-noteDetail-backgroundColor
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
@@ -68,6 +68,15 @@ body
padding 5px
margin -5px
border-radius 5px
.flowchart-error, .sequence-error
background-color errorBackgroundColor
color errorTextColor
padding 5px
border-radius 5px
justify-content left
li
label.taskListItem
margin-left -2em
div.math-rendered
text-align center
.math-failed
@@ -102,12 +111,6 @@ a
background-color alpha(#FFC95C, 0.3)
&:visited
color brandColor
&.lineAnchor
padding 0
margin 0
display block
font-size 0
height 0
hr
border-top none
border-bottom solid 1px borderColor
@@ -147,9 +150,6 @@ h6
line-height 1.4em
margin 1em 0 1em
color #777
*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
margin-top 1em
p
line-height 1.6em
margin 0 0 1em
@@ -174,6 +174,8 @@ ul
margin-bottom 1em
li
display list-item
p
margin 0
&>li>ul, &>li>ol
margin 0
&>li>ul
@@ -186,43 +188,57 @@ ol
margin-bottom 1em
li
display list-item
p
margin 0
&>li>ul, &>li>ol
margin 0
code
padding 0.2em 0.4em
background-color #f7f7f7
border-radius 3px
font-size 0.85em
font-size 1em
text-decoration none
margin-right 2px
*:not(a.lineAnchor) + code
margin-left 2px
pre
padding 0.5em !important
border solid 1px alpha(borderColor, 0.5)
border solid 1px #D1D1D1
border-radius 5px
overflow-x auto
margin 0 0 1em
line-height 1.35
display flex
line-height 1.4em
&.flowchart, &.sequence
display flex
justify-content center
background-color white
&.CodeMirror
height initial
&>code
flex 1
overflow-x auto
code
margin 0
background-color inherit
margin 0
padding 0
border none
border-radius 0
pre
border none
margin -5px
&>span.lineNumber
display none
float left
font-size 0.85em
margin 0 0.5em 0 -0.5em
font-size 1em
padding 0.5em 0
margin -0.5em 0.5em -0.5em -0.5em
border-right 1px solid
text-align right
border-top-left-radius 4px
border-bottom-left-radius 4px
&.CodeMirror-gutters
position initial
top initial
left initial
min-height 0 !important
&>span
display block
padding 0 .5em 0 1em
padding 0 .5em 0
table
display block
width 100%
@@ -236,6 +252,7 @@ table
line-height 1.6
border-width 1px 0 2px 1px
border-color borderColor
font-weight bold
&:last-child
border-right solid 1px borderColor
tbody
@@ -251,3 +268,50 @@ table
border-color borderColor
&:last-child
border-right solid 1px borderColor
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #DDDDDD
themeDarkBorder = lighten(themeDarkBackground, 20%)
themeDarkPreview = $ui-dark-noteDetail-backgroundColor
themeDarkTableOdd = themeDarkPreview
themeDarkTableEven = darken(themeDarkPreview, 10%)
themeDarkTableHead = themeDarkTableEven
themeDarkTableBorder = themeDarkBorder
themeDarkModalButtonDefault = themeDarkPreview
themeDarkModalButtonDanger = #BF360C
body[data-theme="dark"]
color themeDarkText
border-color themeDarkBorder
background-color themeDarkPreview
a:hover
background-color alpha(lighten(brandColor, 30%), 0.2) !important
code
border-color darken(themeDarkBorder, 10%)
background-color lighten(themeDarkPreview, 10%)
pre
border-color lighten(#21252B, 20%)
code
background-color transparent
label.taskListItem
background-color themeDarkPreview
table
thead
tr
background-color themeDarkTableHead
th
border-color themeDarkTableBorder
&:last-child
border-right solid 1px themeDarkTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeDarkTableOdd
tr:nth-child(2n)
background-color themeDarkTableEven
td
border-color themeDarkTableBorder
&:last-child
border-right solid 1px themeDarkTableBorder

View File

@@ -1,44 +0,0 @@
import React, { PropTypes } from 'react'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import ModeIcon from 'browser/components/ModeIcon'
export default class FinderDetail extends React.Component {
render () {
let { activeArticle } = this.props
if (activeArticle != null) {
return (
<div className='FinderDetail'>
<div className='header'>
<div className='left'>
<ModeIcon mode={activeArticle.mode}/> {activeArticle.title}
</div>
<div className='right'>
<button onClick={this.props.saveToClipboard} className='clipboardBtn'>
<i className='fa fa-clipboard fa-fw'/>
<span className='tooltip'>Copy to clipboard (Enter)</span>
</button>
</div>
</div>
<div className='content'>
{activeArticle.mode === 'markdown'
? <MarkdownPreview content={activeArticle.content}/>
: <CodeEditor readOnly article={activeArticle}/>
}
</div>
</div>
)
}
return (
<div className='FinderDetail'>
<div className='nothing'>Nothing selected</div>
</div>
)
}
}
FinderDetail.propTypes = {
activeArticle: PropTypes.shape(),
saveToClipboard: PropTypes.func
}

View File

@@ -1,16 +0,0 @@
import React, { PropTypes } from 'react'
export default class FinderInput extends React.Component {
render () {
return (
<div className='FinderInput'>
<input ref='input' value={this.props.value} onChange={this.props.handleSearchChange} type='text'/>
</div>
)
}
}
FinderInput.propTypes = {
handleSearchChange: PropTypes.func,
value: PropTypes.string
}

View File

@@ -1,71 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import { selectArticle } from './actions'
export default class FinderList extends React.Component {
componentDidUpdate () {
var index = this.props.articles.indexOf(this.props.activeArticle)
var el = ReactDOM.findDOMNode(this)
var li = el.querySelectorAll('li')[index]
if (li == null) {
return
}
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
if (overflowBelow) {
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
}
var overflowAbove = el.scrollTop > li.offsetTop
if (overflowAbove) {
el.scrollTop = li.offsetTop
}
}
handleArticleClick (article) {
return (e) => {
let { dispatch } = this.props
dispatch(selectArticle(article.key))
}
}
render () {
let articleElements = this.props.articles.map(function (article) {
if (article == null) {
return (
<li className={isActive ? 'active' : ''}>
<div className='articleItem'>Undefined</div>
<div className='divider'/>
</li>
)
}
var isActive = this.props.activeArticle != null && (article.key === this.props.activeArticle.key)
return (
<li key={'article-' + article.key} onClick={this.handleArticleClick(article)} className={isActive ? 'active' : ''}>
<div className='articleItem'>
<ModeIcon mode={article.mode}/> {article.title}</div>
<div className='divider'/>
</li>
)
}.bind(this))
return (
<div className='FinderList'>
<ul>
{articleElements}
</ul>
</div>
)
}
}
FinderList.propTypes = {
articles: PropTypes.array,
activeArticle: PropTypes.shape({
type: PropTypes.string,
key: PropTypes.string
}),
dispatch: PropTypes.func
}

View File

@@ -0,0 +1,118 @@
$search-height = 50px
$nav-width = 175px
$list-width = 250px
.root
absolute top left right bottom
.search
height $search-height
padding 10px
box-sizing border-box
border-bottom $ui-border
text-align center
.search-input
height 30px
width 100%
margin 0 auto
font-size 18px
border none
outline none
text-align center
background-color transparent
.result
absolute left right bottom
top $search-height
background-color $ui-noteDetail-backgroundColor
.result-nav
user-select none
absolute left top bottom
width $nav-width
background-color $ui-backgroundColor
.result-nav-filter
margin-bottom 10px
.result-nav-filter-option
height 25px
line-height 25px
padding 0 10px
label
cursor pointer
.result-nav-menu
navButtonColor()
height 32px
padding 0 10px
font-size 14px
width 100%
outline none
text-align left
line-height 32px
box-sizing border-box
cursor pointer
.result-nav-menu--active
@extend .result-nav-menu
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.result-nav-storageList
absolute bottom left right
top 80px + 32px + 10px + 10px
overflow-y auto
.result-list
user-select none
absolute top bottom
left $nav-width
width $list-width
box-sizing border-box
overflow-y auto
box-shadow 2px 0 15px -8px #b1b1b1
z-index 1
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-noteDetail-backgroundColor
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
.search
border-color $ui-dark-borderColor
.search-input
color $ui-dark-text-color
.result
background-color $ui-dark-noteList-backgroundColor
.result-nav
background-color $ui-dark-backgroundColor
label
color $ui-dark-text-color
.result-nav-menu
navDarkButtonColor()
.result-nav-menu--active
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
.result-list
border-color $ui-dark-borderColor
box-shadow none
top 0
.result-detail
absolute top bottom right
left $nav-width + $list-width
background-color $ui-dark-noteDetail-backgroundColor

View File

@@ -0,0 +1,203 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteDetail.styl'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import CodeEditor from 'browser/components/CodeEditor'
import CodeMirror from 'codemirror'
const electron = require('electron')
const { clipboard } = electron
const path = require('path')
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
function notify (title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
}
return new window.Notification(title, options)
}
class NoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
snippetIndex: 0
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.note !== this.props.note) {
this.setState({
snippetIndex: 0
}, () => {
if (nextProps.note.type === 'SNIPPET_NOTE') {
nextProps.note.snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
}
})
}
}
selectPriorSnippet () {
let { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + note.snippets.length - 1) % note.snippets.length
})
}
}
selectNextSnippet () {
let { note } = this.props
if (note.type === 'SNIPPET_NOTE' && note.snippets.length > 1) {
this.setState({
snippetIndex: (this.state.snippetIndex + 1) % note.snippets.length
})
}
}
saveToClipboard () {
let { note } = this.props
if (note.type === 'MARKDOWN_NOTE') {
clipboard.writeText(note.content)
} else {
clipboard.writeText(note.snippets[this.state.snippetIndex].content)
}
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
}
handleTabButtonClick (e, index) {
this.setState({
snippetIndex: index
})
}
render () {
let { note, config } = this.props
if (note == null) {
return (
<div styleName='root' />
)
}
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
if (note.type === 'SNIPPET_NOTE') {
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(e, index)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
</div>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
{snippet.mode === 'markdown'
? <MarkdownEditor styleName='tabView-content'
config={config}
value={snippet.content}
ref={'code-' + index}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
keyMap={config.editor.keyMap}
readOnly
ref={'code-' + index}
/>
}
</div>
})
return (
<div styleName='root'>
<div styleName='description'>
<textarea styleName='description-textarea'
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={note.description}
readOnly
/>
</div>
<div styleName='tabList'>
{tabList}
</div>
{viewList}
</div>
)
}
return (
<MarkdownPreview styleName='root'
theme={config.ui.theme}
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
value={note.content}
/>
)
}
}
NoteDetail.propTypes = {
}
export default CSSModules(NoteDetail, styles)

View File

@@ -0,0 +1,97 @@
@import('../main/Detail/DetailVars.styl')
.root
absolute top bottom left right
left $note-detail-left-margin
right $note-detail-right-margin
height 100%
width 365px
background-color $ui-noteDetail-backgroundColor
.description
absolute top left right
height 80px
box-sizing border-box
.description-textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
box-sizing border-box
background-color $ui-noteDetail-backgroundColor
.tabList
absolute left right
top 80px
box-sizing border-box
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
.tabList-item
position relative
flex 1
overflow hidden
&:hover
background-color $ui-button--hover-backgroundColorg
.tabList-item--active
@extend .tabList-item
border-bottom $ui-border
.tabList-item-button
width 100%
height 29px
overflow ellipsis
text-align left
padding-right 30px
padding-left 10px
border none
background-color transparent
transition 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
.tabView
absolute left right bottom
top 130px
.tabView-content
absolute top left right bottom
box-sizing border-box
height 100%
width 100%
body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
.description
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
.description-textarea
background-color $ui-dark-noteDetail-backgroundColor
color white
.tabList
background-color $ui-dark-noteDetail-backgroundColor
.tabList-item
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.tabList-item-button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color white
background-color $ui-dark-button--hover-backgroundColor

View File

@@ -0,0 +1,91 @@
import React from 'react'
import NoteItem from 'browser/components/NoteItem'
import moment from 'moment'
class NoteList extends React.Component {
constructor (props) {
super(props)
this.state = {
range: 0
}
}
componentWillReceiveProps (nextProps) {
if (this.props.search !== nextProps.search) {
this.resetScroll()
}
}
componentDidUpdate () {
let { index } = this.props
if (index > -1) {
let list = this.refs.root
let item = list.childNodes[index]
if (item == null) return null
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
}
let overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
resetScroll () {
this.refs.root.scrollTop = 0
this.setState({
range: 0
})
}
handleScroll (e) {
let { notes } = this.props
if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 100 && notes.length > this.state.range * 10 + 10) {
this.setState({
range: this.state.range + 1
})
}
}
render () {
let { notes, index } = this.props
let notesList = notes
.slice(0, 10 + 10 * this.state.range)
.map((note, _index) => {
const isActive = (index === _index)
const key = `${note.storage}-${note.key}`
const dateDisplay = moment(note.updatedAt).fromNow()
return (
<NoteItem
isActive={isActive}
note={note}
dateDisplay={dateDisplay}
key={key}
handleNoteClick={(e) => this.props.handleNoteClick(e, _index)}
handleNoteContextMenu={() => ''}
/>
)
})
return (
<div className={this.props.className}
onScroll={(e) => this.handleScroll(e)}
ref='root'
>
{notesList}
</div>
)
}
}
NoteList.propTypes = {
}
export default NoteList

View File

@@ -0,0 +1,77 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageSection.styl'
import StorageItem from 'browser/components/StorageItem'
class StorageSection extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleHeaderClick (e) {
let { storage } = this.props
this.props.handleStorageButtonClick(e, storage.key)
}
handleFolderClick (e, folder) {
let { storage } = this.props
this.props.handleFolderButtonClick(e, storage.key, folder.key)
}
render () {
let { storage, filter } = this.props
let folderList = storage.folders
.map(folder => (
<StorageItem
key={folder.key}
isActive={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key}
handleButtonClick={(e) => this.handleFolderClick(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={false}
/>
))
return (
<div styleName='root'>
<div styleName='header'>
<button styleName='header-toggleButton'
onClick={(e) => this.handleToggleButtonClick(e)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
}
/>
</button>
<button styleName={filter.type === 'STORAGE' && filter.storage === storage.key
? 'header-name--active'
: 'header-name'
}
onClick={(e) => this.handleHeaderClick(e)}
>{storage.name}</button>
</div>
{this.state.isOpen &&
<div styleName='folderList'>
{folderList}
</div>
}
</div>
)
}
}
StorageSection.propTypes = {
}
export default CSSModules(StorageSection, styles)

View File

@@ -0,0 +1,85 @@
.root
position relative
.header
height 26px
.header-toggleButton
absolute top left
width 25px
height 26px
navButtonColor()
border none
outline none
.header-name
display block
height 26px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 26px
box-sizing border-box
cursor pointer
outline none
.header-name--active
@extend .header-name
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.folderList-item
display block
width 100%
height 26px
navButtonColor()
padding 0 10px 0 25px
font-size 14px
width 100%
text-align left
line-height 26px
box-sizing border-box
cursor pointer
outline none
padding 0 10px
margin 2px 0
height 26px
line-height 26px
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item--active
@extend .folderList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
body[data-theme="dark"]
.header-toggleButton
navDarkButtonColor()
.header-name
navDarkButtonColor()
.header-name--active
@extend .header-name
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.folderList-item
navDarkButtonColor()
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item--active
@extend .folderList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor

View File

@@ -1,39 +0,0 @@
export const SELECT_ARTICLE = 'SELECT_ARTICLE'
export const SEARCH_ARTICLE = 'SEARCH_ARTICLE'
export const REFRESH_DATA = 'REFRESH_DATA'
export function selectArticle (key) {
return {
type: SELECT_ARTICLE,
data: { key }
}
}
export function searchArticle (input) {
return {
type: SEARCH_ARTICLE,
data: { input }
}
}
export function refreshData (data) {
console.log('refreshing data')
let { folders, articles } = data
return {
type: REFRESH_DATA,
data: {
articles,
folders
}
}
}
export default {
SELECT_ARTICLE,
SEARCH_ARTICLE,
REFRESH_DATA,
selectArticle,
searchArticle,
refreshData
}

View File

@@ -1,85 +1,87 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import { connect, Provider } from 'react-redux'
import reducer from './reducer'
import { createStore } from 'redux'
import FinderInput from './FinderInput'
import FinderList from './FinderList'
import FinderDetail from './FinderDetail'
import actions, { selectArticle, searchArticle } from './actions'
import _ from 'lodash'
import dataStore from 'browser/lib/dataStore'
import fetchConfig from '../lib/fetchConfig'
import ipc from './ipcClient'
import store from './store'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FinderMain.styl'
import StorageSection from './StorageSection'
import NoteList from './NoteList'
import NoteDetail from './NoteDetail'
import SideNavFilter from 'browser/components/SideNavFilter'
require('!!style!css!stylus?sourceMap!../main/global.styl')
require('../lib/customMeta')
const electron = require('electron')
const { clipboard, ipcRenderer, remote } = electron
const path = require('path')
let config = fetchConfig()
applyConfig(config)
ipcRenderer.on('config-apply', function (e, newConfig) {
config = newConfig
applyConfig(config)
})
function applyConfig () {
let body = document.body
body.setAttribute('data-theme', config['theme-ui'])
let hljsCss = document.getElementById('hljs-css')
hljsCss.setAttribute('href', '../node_modules/highlight.js/styles/' + config['theme-code'] + '.css')
}
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
const { remote } = electron
const { Menu } = remote
function hideFinder () {
ipcRenderer.send('hide-finder')
let finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'win32') {
finderWindow.blur()
finderWindow.hide()
}
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
if (global.process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
}
return new window.Notification(title, options)
remote.getCurrentWindow().hide()
}
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TEXT_FILTER = 'TEXT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
class FinderMain extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
index: 0,
filter: {
includeSnippet: true,
includeMarkdown: false,
type: 'ALL',
storage: null,
folder: null
}
}
this.focusHandler = (e) => this.handleWindowFocus(e)
this.blurHandler = (e) => this.handleWindowBlur(e)
}
componentDidMount () {
this.keyDownHandler = e => this.handleKeyDown(e)
document.addEventListener('keydown', this.keyDownHandler)
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
this.focusHandler = e => {
let { dispatch } = this.props
dispatch(searchArticle(''))
dispatch(selectArticle(null))
}
this.refs.search.focus()
window.addEventListener('focus', this.focusHandler)
window.addEventListener('blur', this.blurHandler)
}
componentWillUnmount () {
document.removeEventListener('keydown', this.keyDownHandler)
window.removeEventListener('focus', this.focusHandler)
window.removeEventListener('blur', this.blurHandler)
}
handleWindowFocus (e) {
this.refs.search.focus()
}
handleWindowBlur (e) {
this.setState({
search: ''
})
}
handleKeyDown (e) {
this.refs.search.focus()
if (e.keyCode === 9) {
if (e.shiftKey) {
this.refs.detail.selectPriorSnippet()
} else {
this.refs.detail.selectNextSnippet()
}
e.preventDefault()
}
if (e.keyCode === 38) {
this.selectPrevious()
e.preventDefault()
@@ -91,7 +93,8 @@ class FinderMain extends React.Component {
}
if (e.keyCode === 13) {
this.saveToClipboard()
this.refs.detail.saveToClipboard()
hideFinder()
e.preventDefault()
}
if (e.keyCode === 27) {
@@ -101,26 +104,13 @@ class FinderMain extends React.Component {
if (e.keyCode === 91 || e.metaKey) {
return
}
ReactDOM.findDOMNode(this.refs.finderInput.refs.input).focus()
}
saveToClipboard () {
let { activeArticle } = this.props
clipboard.writeText(activeArticle.content)
ipcRenderer.send('copy-finder')
notify('Saved to Clipboard!', {
body: 'Paste it wherever you want!',
silent: true
})
hideFinder()
}
handleSearchChange (e) {
let { dispatch } = this.props
dispatch(searchArticle(e.target.value))
this.setState({
search: e.target.value,
index: 0
})
}
selectArticle (article) {
@@ -128,151 +118,231 @@ class FinderMain extends React.Component {
}
selectPrevious () {
let { activeArticle, dispatch } = this.props
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
let previousArticle = this.refs.finderList.props.articles[index - 1]
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
if (this.state.index > 0) {
this.setState({
index: this.state.index - 1
})
}
}
selectNext () {
let { activeArticle, dispatch } = this.props
let index = this.refs.finderList.props.articles.indexOf(activeArticle)
let previousArticle = this.refs.finderList.props.articles[index + 1]
if (previousArticle != null) dispatch(selectArticle(previousArticle.key))
if (this.state.index < this.noteCount - 1) {
this.setState({
index: this.state.index + 1
})
}
}
handleOnlySnippetCheckboxChange (e) {
let { filter } = this.state
filter.includeSnippet = e.target.checked
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleOnlyMarkdownCheckboxChange (e) {
let { filter } = this.state
filter.includeMarkdown = e.target.checked
this.refs.list.resetScroll()
this.setState({
filter: filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleAllNotesButtonClick (e) {
let { filter } = this.state
filter.type = 'ALL'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStarredButtonClick (e) {
let { filter } = this.state
filter.type = 'STARRED'
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleStorageButtonClick (e, storage) {
let { filter } = this.state
filter.type = 'STORAGE'
filter.storage = storage
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleFolderButtonClick (e, storage, folder) {
let { filter } = this.state
filter.type = 'FOLDER'
filter.storage = storage
filter.folder = folder
this.refs.list.resetScroll()
this.setState({
filter,
index: 0
}, () => {
this.refs.search.focus()
})
}
handleNoteClick (e, index) {
this.setState({
index
}, () => {
this.refs.search.focus()
})
}
render () {
let { articles, activeArticle, status, dispatch } = this.props
let saveToClipboard = () => this.saveToClipboard()
let { data, config } = this.props
let { filter, search } = this.state
let storageList = []
for (let key in data.storageMap) {
let storage = data.storageMap[key]
let item = (
<StorageSection
filter={filter}
storage={storage}
key={storage.key}
handleStorageButtonClick={(e, storage) => this.handleStorageButtonClick(e, storage)}
handleFolderButtonClick={(e, storage, folder) => this.handleFolderButtonClick(e, storage, folder)}
/>
)
storageList.push(item)
}
let notes = []
let noteIds
switch (filter.type) {
case 'STORAGE':
noteIds = data.storageNoteMap[filter.storage]
break
case 'FOLDER':
noteIds = data.folderNoteMap[filter.storage + '-' + filter.folder]
break
case 'STARRED':
noteIds = data.starredSet
}
if (noteIds != null) {
noteIds.forEach((id) => {
notes.push(data.noteMap[id])
})
} else {
for (let key in data.noteMap) {
notes.push(data.noteMap[key])
}
}
if (!filter.includeSnippet && filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'MARKDOWN_NOTE')
} else if (filter.includeSnippet && !filter.includeMarkdown) {
notes = notes.filter((note) => note.type === 'SNIPPET_NOTE')
}
if (search.trim().length > 0) {
let needle = new RegExp(_.escapeRegExp(search.trim()), 'i')
notes = notes.filter((note) => note.title.match(needle))
}
notes = notes
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
let activeNote = notes[this.state.index]
this.noteCount = notes.length
return (
<div className='Finder'>
<FinderInput
handleSearchChange={e => this.handleSearchChange(e)}
ref='finderInput'
onChange={this.handleChange}
value={status.search}
<div className='Finder'
styleName='root'
ref='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='search'>
<input
styleName='search-input'
ref='search'
value={search}
placeholder='Search...'
onChange={(e) => this.handleSearchChange(e)}
/>
<FinderList
ref='finderList'
activeArticle={activeArticle}
articles={articles}
dispatch={dispatch}
selectArticle={article => this.selectArticle(article)}
</div>
<div styleName='result'>
<div styleName='result-nav'>
<div styleName='result-nav-filter'>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeSnippet}
onChange={(e) => this.handleOnlySnippetCheckboxChange(e)}
/> Only Snippets</label>
</div>
<div styleName='result-nav-filter-option'>
<label>
<input type='checkbox'
checked={filter.includeMarkdown}
onChange={(e) => this.handleOnlyMarkdownCheckboxChange(e)}
/> Only Markdown</label>
</div>
</div>
<SideNavFilter
isHomeActive={filter.type === 'ALL'}
handleAllNotesButtonClick={(e) => this.handleAllNotesButtonClick(e)}
isStarredActive={filter.type === 'STARRED'}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
/>
<FinderDetail
activeArticle={activeArticle}
saveToClipboard={saveToClipboard}
<div styleName='result-nav-storageList'>
{storageList}
</div>
</div>
<NoteList styleName='result-list'
storageMap={data.storageMap}
notes={notes}
ref='list'
search={search}
index={this.state.index}
handleNoteClick={(e, _index) => this.handleNoteClick(e, _index)}
/>
<div styleName='result-detail'>
<NoteDetail
note={activeNote}
config={config}
ref='detail'
/>
</div>
</div>
</div>
)
}
}
FinderMain.propTypes = {
articles: PropTypes.array,
activeArticle: PropTypes.shape({
key: PropTypes.string,
tags: PropTypes.array,
title: PropTypes.string,
content: PropTypes.string
}),
status: PropTypes.shape(),
dispatch: PropTypes.func
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/)
}
// Build filter object by key
function buildFilter (key) {
if (key.match(/^\/\/.+/)) {
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
}
if (key.match(/^\/.+/)) {
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
}
function isContaining (target, needle) {
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
}
function startsWith (target, needle) {
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
}
function remap (state) {
let { articles, folders, status } = state
let filters = status.search.split(' ')
.map(key => key.trim())
.filter(ignoreInvalidKey)
.map(buildFilter)
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
let targetFolders
if (folders != null) {
let exactTargetFolders = folders.filter(folder => {
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
})
let fuzzyTargetFolders = folders.filter(folder => {
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
})
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
if (targetFolders.length > 0) {
articles = articles.filter(article => {
return _.findWhere(targetFolders, {key: article.FolderKey})
})
}
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
})
}, articles)
}
}
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
return {
articles,
activeArticle,
status
}
}
var Finder = connect(remap)(FinderMain)
var store = createStore(reducer)
var Finder = connect((x) => x)(CSSModules(FinderMain, styles))
function refreshData () {
let data = dataStore.getData(true)
store.dispatch(actions.refreshData(data))
}
window.onfocus = e => {
refreshData()
// let data = dataStore.getData(true)
}
ReactDOM.render((

122
browser/finder/ipcClient.js Normal file
View File

@@ -0,0 +1,122 @@
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app, Menu } = remote
const path = require('path')
const store = require('./store')
const consts = require('browser/lib/consts')
nodeIpc.config.id = 'finder'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
function killFinder () {
let finderWindow = remote.getCurrentWindow()
finderWindow.removeAllListeners()
if (global.process.platform === 'darwin') {
// Only OSX has another app process.
nodeIpc.of.node.emit('quit-from-finder')
} else {
finderWindow.close()
}
}
function toggleFinder () {
let finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'darwin') {
if (finderWindow.isVisible()) {
finderWindow.hide()
Menu.sendActionToFirstResponder('hide:')
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
}
} else {
if (finderWindow.isVisible()) {
finderWindow.blur()
finderWindow.hide()
} else {
nodeIpc.of.node.emit('request-data-from-finder')
finderWindow.show()
finderWindow.focus()
}
}
}
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
console.log(err)
})
nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully')
})
nodeIpc.of.node.on('disconnect', function () {
console.log('disconnected')
})
nodeIpc.of.node.on('open-finder', function () {
toggleFinder()
})
ipcRenderer.on('open-finder-from-tray', function () {
toggleFinder()
})
ipcRenderer.on('open-main-from-tray', function () {
nodeIpc.of.node.emit('open-main-from-finder')
})
ipcRenderer.on('quit-from-tray', function () {
nodeIpc.of.node.emit('quit-from-finder')
killFinder()
})
nodeIpc.of.node.on('throttle-data', function (payload) {
console.log('Received data from Main renderer')
store.default.dispatch({
type: 'THROTTLE_DATA',
data: payload
})
})
nodeIpc.of.node.on('config-renew', function (payload) {
const { config } = payload
if (config.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
editorTheme.setAttribute('id', 'editorTheme')
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
if (config.editor.theme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
store.default.dispatch({
type: 'SET_CONFIG',
config: config
})
})
nodeIpc.of.node.on('quit-finder-app', function () {
nodeIpc.of.node.emit('quit-finder-app-confirm')
killFinder()
})
}
)
const ipc = {}
module.exports = ipc

View File

@@ -1,47 +0,0 @@
import { combineReducers } from 'redux'
import { SELECT_ARTICLE, SEARCH_ARTICLE, REFRESH_DATA } from './actions'
let initialArticles = []
let initialFolders = []
let initialStatus = {
articleKey: null,
search: ''
}
function status (state = initialStatus, action) {
switch (action.type) {
case SELECT_ARTICLE:
state.articleKey = action.data.key
return Object.assign({}, state)
case SEARCH_ARTICLE:
state.search = action.data.input
return Object.assign({}, state)
default:
return state
}
}
function articles (state = initialArticles, action) {
switch (action.type) {
case REFRESH_DATA:
return action.data.articles
default:
return state
}
}
function folders (state = initialFolders, action) {
switch (action.type) {
case REFRESH_DATA:
console.log(action)
return action.data.folders
default:
return state
}
}
export default combineReducers({
status,
folders,
articles
})

51
browser/finder/store.js Normal file
View File

@@ -0,0 +1,51 @@
import { combineReducers, createStore } from 'redux'
import { routerReducer } from 'react-router-redux'
import { DEFAULT_CONFIG } from 'browser/main/lib/ConfigManager'
let defaultData = {
storageMap: {},
noteMap: {},
starredSet: [],
storageNoteMap: {},
folderNoteMap: {},
tagNoteMap: {}
}
function data (state = defaultData, action) {
switch (action.type) {
case 'THROTTLE_DATA':
console.log(action)
state = action.data
}
return state
}
function config (state = DEFAULT_CONFIG, action) {
switch (action.type) {
case 'INIT_CONFIG':
case 'SET_CONFIG':
return Object.assign({}, state, action.config)
case 'SET_IS_SIDENAV_FOLDED':
state.isSideNavFolded = action.isFolded
return Object.assign({}, state)
case 'SET_ZOOM':
state.zoom = action.zoom
return Object.assign({}, state)
case 'SET_LIST_WIDTH':
state.listWidth = action.listWidth
return Object.assign({}, state)
case 'SET_UI':
return Object.assign({}, state, action.config)
}
return state
}
let reducer = combineReducers({
data,
config,
routing: routerReducer
})
let store = createStore(reducer)
export default store

106
browser/lib/Mutable.js Normal file
View File

@@ -0,0 +1,106 @@
class MutableMap {
constructor (iterable) {
this._map = new Map(iterable)
Object.defineProperty(this, 'size', {
get: () => this._map.size,
set: function (value) {
this['size'] = value
}
})
}
get (...args) {
return this._map.get(...args)
}
set (...args) {
return this._map.set(...args)
}
delete (...args) {
return this._map.delete(...args)
}
has (...args) {
return this._map.has(...args)
}
clear (...args) {
return this._map.clear(...args)
}
forEach (...args) {
return this._map.forEach(...args)
}
[Symbol.iterator] () {
return this._map[Symbol.iterator]()
}
map (cb) {
let result = []
for (let [key, value] of this._map) {
result.push(cb(value, key))
}
return result
}
toJS () {
let result = {}
for (let [key, value] of this._map) {
if (value instanceof MutableSet || value instanceof MutableMap) {
value = value.toJS()
}
result[key] = value
}
return result
}
}
class MutableSet {
constructor (iterable) {
this._set = new Set(iterable)
Object.defineProperty(this, 'size', {
get: () => this._set.size,
set: function (value) {
this['size'] = value
}
})
}
add (...args) {
return this._set.add(...args)
}
delete (...args) {
return this._set.delete(...args)
}
forEach (...args) {
return this._set.forEach(...args)
}
[Symbol.iterator] () {
return this._set[Symbol.iterator]()
}
map (cb) {
let result = []
this._set.forEach(function (value, key) {
result.push(cb(value, key))
})
return result
}
toJS () {
return Array.from(this._set)
}
}
const Mutable = {
Map: MutableMap,
Set: MutableSet
}
module.exports = Mutable

View File

@@ -1,138 +0,0 @@
import _ from 'lodash'
import moment from 'moment'
import dataStore from './dataStore'
import { request, SERVER_URL } from './api'
import clientKey from './clientKey'
const electron = require('electron')
const version = electron.remote.app.getVersion()
function isSameDate (a, b) {
a = moment(a).utcOffset(+540).format('YYYYMMDD')
b = moment(b).utcOffset(+540).format('YYYYMMDD')
return a === b
}
export function init () {
let records = getAllRecords()
if (records == null) {
saveAllRecords([])
}
emit(null)
postRecords()
if (window != null) {
window.addEventListener('online', postRecords)
window.setInterval(postRecords, 1000 * 60 * 60 * 24)
}
}
export function getAllRecords () {
return JSON.parse(localStorage.getItem('activityRecords'))
}
export function saveAllRecords (records) {
localStorage.setItem('activityRecords', JSON.stringify(records))
}
/*
Post all records(except today)
and remove all posted records
*/
export function postRecords (data) {
if (process.env.NODE_ENV !== 'production') {
console.log('post failed - NOT PRODUCTION ')
return
}
let records = getAllRecords()
records = records.filter(record => {
return !isSameDate(new Date(), record.date)
})
if (records.length === 0) {
console.log('No records to post')
return
}
console.log('posting...', records)
let input = {
clientKey: clientKey.get(),
records
}
return request.post(SERVER_URL + 'apis/activity')
.send(input)
.then(res => {
let records = getAllRecords()
let todayRecord = _.find(records, record => {
return isSameDate(new Date(), record.date)
})
if (todayRecord != null) saveAllRecords([todayRecord])
else saveAllRecords([])
})
.catch(err => {
console.error(err)
})
}
export function emit (type, data = {}) {
let records = getAllRecords()
let index = _.findIndex(records, record => {
return isSameDate(new Date(), record.date)
})
let todayRecord
if (index < 0) {
todayRecord = {date: new Date()}
records.push(todayRecord)
}
else todayRecord = records[index]
switch (type) {
case 'ARTICLE_CREATE':
case 'ARTICLE_UPDATE':
case 'ARTICLE_DESTROY':
case 'FOLDER_CREATE':
case 'FOLDER_UPDATE':
case 'FOLDER_DESTROY':
case 'FINDER_OPEN':
case 'FINDER_COPY':
case 'MAIN_DETAIL_COPY':
case 'ARTICLE_SHARE':
todayRecord[type] = todayRecord[type] == null
? 1
: todayRecord[type] + 1
break
}
// Count ARTICLE_CREATE and ARTICLE_UPDATE again by syntax
if (type === 'ARTICLE_UPDATE' && data.mode != null) {
let recordKey = type + '_BY_SYNTAX'
if (todayRecord[recordKey] == null) todayRecord[recordKey] = {}
todayRecord[recordKey][data.mode] = todayRecord[recordKey][data.mode] == null
? 1
: todayRecord[recordKey][data.mode] + 1
}
let storeData = dataStore.getData()
todayRecord.FOLDER_COUNT = storeData && _.isArray(storeData.folders) ? storeData.folders.length : 0
todayRecord.ARTICLE_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.length : 0
todayRecord.CLIENT_VERSION = version
todayRecord.SYNTAX_COUNT = storeData && _.isArray(storeData.articles) ? storeData.articles.reduce((sum, article) => {
if (sum[article.mode] == null) sum[article.mode] = 1
else sum[article.mode]++
return sum
}, {}) : 0
saveAllRecords(records)
}
export default {
init,
emit,
postRecords
}

View File

@@ -1,21 +0,0 @@
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
}

View File

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

View File

@@ -1,3 +1,16 @@
const path = require('path')
const fs = require('sander')
const { remote } = require('electron')
const { app } = remote
const themePath = process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
: require('path').resolve('./node_modules/codemirror/theme')
const themes = fs.readdirSync(themePath)
.map((themePath) => {
return themePath.substring(0, themePath.lastIndexOf('.'))
})
const consts = {
FOLDER_COLORS: [
'#E10051',
@@ -16,7 +29,8 @@ const consts = {
'Turquoise',
'Dodger Blue',
'Violet Eggplant'
]
],
THEMES: ['default'].concat(themes)
}
module.exports = consts

17
browser/lib/context.js Normal file
View File

@@ -0,0 +1,17 @@
const { remote } = require('electron')
const { Menu, MenuItem } = remote
function popup (templates) {
let menu = new Menu()
templates.forEach((item) => {
menu.append(new MenuItem(item))
})
menu.popup(remote.getCurrentWindow())
}
const context = {
popup
}
module.export = context
export default context

View File

@@ -0,0 +1,3 @@
import CodeMirror from 'codemirror'
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})

View File

@@ -1,156 +0,0 @@
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,18 @@
/**
* @fileoverview Formatting date string.
*/
import moment from 'moment'
/**
* @description Return date string. For example, 'Sep.9, 2016 12:00'.
* @param {mixed}
* @return {string}
*/
export function getLastUpdated (date) {
const m = moment(date)
if (!m.isValid()) {
throw Error('Invalid argument.')
}
return m.format('MMM D, gggg H:mm')
}

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import markdownit from 'markdown-it'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
import _ from 'lodash'
const katex = window.katex
@@ -9,9 +9,9 @@ function createGutter (str) {
let lc = (str.match(/\n/g) || []).length
let lines = []
for (let i = 1; i <= lc; i++) {
lines.push('<span>' + i + '</span>')
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
}
return '<span class="lineNumber">' + lines.join('') + '</span>'
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
}
var md = markdownit({
@@ -20,19 +20,16 @@ var md = markdownit({
html: true,
xhtmlOut: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return '<pre class="hljs">' +
createGutter(str) +
'<code>' +
hljs.highlight(lang, str).value +
'</code></pre>'
} catch (e) {}
if (lang === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
return '<pre class="hljs">' +
if (lang === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code">' +
createGutter(str) +
'<code>' +
str.replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;') +
'<code class="' + lang + '">' +
str +
'</code></pre>'
}
})
@@ -59,21 +56,109 @@ md.use(math, {
return output
}
})
md.use(require('markdown-it-checkbox'))
md.use(require('markdown-it-footnote'))
// Override task item
md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
let content, terminate, i, l, token
let nextLine = startLine + 1
let terminatorRules = state.md.block.ruler.getRules('paragraph')
let endLine = state.lineMax
let originalRenderToken = md.renderer.renderToken
md.renderer.renderToken = function renderToken (tokens, idx, options) {
let token = tokens[idx]
// jump line-by-line until empty one or EOF
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue }
let result = originalRenderToken.call(md.renderer, tokens, idx, options)
if (token.map != null) {
return result + '<a class=\'lineAnchor\' data-key=\'' + token.map[0] + '\'></a>'
// quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue }
// Some tags can terminate paragraph without empty line.
terminate = false
for (i = 0, l = terminatorRules.length; i < l; i++) {
if (terminatorRules[i](state, nextLine, endLine, true)) {
terminate = true
break
}
}
if (terminate) { break }
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
token = state.push('paragraph_open', 'p', 1)
token.map = [ startLine, state.line ]
if (state.parentType === 'list') {
let match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) {
content = `<label class='taskListItem' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
token = state.push('inline', '', 0)
token.content = content
token.map = [ startLine, state.line ]
token.children = []
token = state.push('paragraph_close', 'p', -1)
return true
})
// Add line number attribute for scrolling
let originalRender = md.renderer.render
md.renderer.render = function render (tokens, options, env) {
tokens.forEach((token) => {
switch (token.type) {
case 'heading_open':
case 'paragraph_open':
case 'blockquote_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
})
let result = originalRender.call(md.renderer, tokens, options, env)
return result
}
window.md = md
export default function markdown (content) {
if (content == null) content = ''
return md.render(content.toString())
function strip (input) {
var output = input
try {
output = output
.replace(/^([\s\t]*)([\*\-\+]|\d\.)\s+/gm, '$1')
.replace(/\n={2,}/g, '\n')
.replace(/~~/g, '')
.replace(/`{3}.*\n/g, '')
.replace(/<(.*?)>/g, '$1')
.replace(/^[=\-]{2,}\s*$/g, '')
.replace(/\[\^.+?\](: .*?$)?/g, '')
.replace(/\s{0,2}\[.*?\]: .*?$/g, '')
.replace(/!\[.*?\][\[\(].*?[\]\)]/g, '')
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
.replace(/>/g, '')
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
.replace(/([\*_]{1,3})(\S.*?\S)\1/g, '$2')
.replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1')
.replace(/\n{2,}/g, '\n\n')
} catch (e) {
console.error(e)
return input
}
return output
}
const markdown = {
render: function markdown (content) {
if (!_.isString(content)) content = ''
return md.render(content)
},
strip
}
export default markdown

View File

@@ -1,753 +0,0 @@
const modes = [
{
name: 'text',
label: 'Plain text',
mode: 'text'
},
{
name: 'abap',
label: 'ABAP',
alias: [],
mode: 'abap'
},
{
name: 'abc',
label: 'ABC',
alias: [],
mode: 'abc'
},
{
name: 'actionscript',
label: 'ActionScript',
alias: ['as'],
mode: 'actionscript'
},
{
name: 'ada',
label: 'Ada',
alias: [],
mode: 'ada'
},
{
name: 'apache_conf',
label: 'Apache config',
alias: ['apache', 'conf'],
mode: 'apache_conf'
},
{
name: 'applescript',
label: 'AppleScript',
alias: ['scpt'],
mode: 'applescript'
},
{
name: 'asciidoc',
label: 'AsciiDoc',
alias: ['ascii', 'doc', 'txt'],
mode: 'asciidoc'
},
{
name: 'assembly_x86',
label: 'Assembly x86',
alias: ['assembly', 'x86', 'asm'],
mode: 'assembly_x86'
},
{
name: 'autohotkey',
label: 'AutoHotkey',
alias: ['ahk'],
mode: 'autohotkey'
},
{
name: 'batchfile',
label: 'Batch file',
alias: ['dos', 'windows', 'bat', 'cmd', 'btm'],
mode: 'batchfile'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cirru',
label: 'Cirru',
alias: [],
mode: 'cirru'
},
{
name: 'clojure',
label: 'Clojure',
alias: ['clj', 'cljs', 'cljc', 'edn'],
mode: 'clojure'
},
{
name: 'cobol',
label: 'COBOL',
alias: ['cbl', 'cob', 'cpy'],
mode: 'cobol'
},
{
name: 'coffee',
label: 'CoffeeScript',
alias: ['coffee'],
mode: 'coffee'
},
{
name: 'coldfusion',
label: 'ColdFusion',
alias: ['cfm', 'cfc'],
mode: 'coldfusion'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'curly',
label: 'Curly',
alias: [],
mode: 'curly'
},
{
name: 'd',
label: 'D',
alias: ['dlang'],
mode: 'd'
},
{
name: 'dockerfile',
label: 'DockerFile',
alias: ['docker'],
mode: 'docker'
},
{
name: 'dart',
label: 'Dart',
alias: [],
mode: 'dart'
},
{
name: 'diff',
label: 'Diff',
alias: [],
mode: 'diff'
},
{
name: 'django',
label: 'Django',
alias: [],
mode: 'djt'
},
{
name: 'dot',
label: 'DOT',
alias: ['gv'],
mode: 'dot'
},
{
name: 'eiffel',
label: 'Eiffel',
alias: [],
mode: 'eiffel'
},
{
name: 'ejs',
label: 'EJS',
alias: [],
mode: 'ejs'
},
{
name: 'elixir',
label: 'Elixir',
alias: ['ex', 'exs'],
mode: 'elixir'
},
{
name: 'elm',
label: 'Elm',
alias: [],
mode: 'elm'
},
{
name: 'erlang',
label: 'Erlang',
alias: ['erl', 'hrl'],
mode: 'erlang'
},
{
name: 'forth',
label: 'Forth',
alias: ['fs', 'fth'],
mode: 'forth'
},
{
name: 'freemaker',
label: 'Freemaker',
alias: ['ftl'],
mode: 'ftl'
},
{
name: 'gcode',
label: 'G-code',
alias: ['mpt', 'mpf', 'nc'],
mode: 'gcode'
},
{
name: 'gherkin',
label: 'Gherkin',
alias: ['cucumber'],
mode: 'gherkin'
},
{
name: 'gitignore',
label: 'Gitignore',
alias: ['git'],
mode: 'gitignore'
},
{
name: 'glsl',
label: 'GLSL',
alias: ['opengl', 'shading'],
mode: 'glsl'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
{
name: 'groovy',
label: 'Groovy',
alias: [],
mode: 'grooby'
},
{
name: 'haml',
label: 'Haml',
alias: [],
mode: 'haml'
},
{
name: 'handlebars',
label: 'Handlebars',
alias: ['hbs'],
mode: 'handlebars'
},
{
name: 'haskell',
label: 'Haskell',
alias: ['hs', 'lhs'],
mode: 'haskell'
},
{
name: 'haxe',
label: 'Haxe',
alias: ['hx', 'hxml'],
mode: 'haxe'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'html_ruby',
label: 'HTML (Ruby)',
alias: ['erb', 'rhtml'],
mode: 'html_ruby'
},
{
name: 'jsx',
label: 'JSX',
alias: ['es', 'babel', 'js', 'jsx', 'react'],
mode: 'jsx'
},
{
name: 'typescript',
label: 'TypeScript',
alias: ['ts'],
mode: 'typescript'
},
{
name: 'ini',
label: 'INI file',
alias: [],
mode: 'ini'
},
{
name: 'io',
label: 'Io',
alias: [],
mode: 'io'
},
{
name: 'jack',
label: 'Jack',
alias: [],
mode: 'jack'
},
{
name: 'jade',
label: 'Jade',
alias: [],
mode: 'jade'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'json',
label: 'JSON',
alias: [],
mode: 'json'
},
{
name: 'jsoniq',
label: 'JSONiq',
alias: ['query'],
mode: 'jsoniq'
},
{
name: 'jsp',
label: 'JSP',
alias: [],
mode: 'jsp'
},
{
name: 'julia',
label: 'Julia',
alias: [],
mode: 'julia'
},
{
name: 'latex',
label: 'Latex',
alias: ['tex'],
mode: 'latex'
},
{
name: 'lean',
label: 'Lean',
alias: [],
mode: 'lean'
},
{
name: 'less',
label: 'Less',
alias: [],
mode: 'less'
},
{
name: 'liquid',
label: 'Liquid',
alias: [],
mode: 'liquid'
},
{
name: 'lisp',
label: 'Lisp',
alias: ['lsp'],
mode: 'lisp'
},
{
name: 'livescript',
label: 'LiveScript',
alias: ['ls'],
mode: 'livescript'
},
{
name: 'logiql',
label: 'LogiQL',
alias: [],
mode: 'logiql'
},
{
name: 'lsl',
label: 'LSL',
alias: [],
mode: 'lsl'
},
{
name: 'lua',
label: 'Lua',
alias: [],
mode: 'lua'
},
{
name: 'luapage',
label: 'Luapage',
alias: [],
mode: 'luapage'
},
{
name: 'lucene',
label: 'Lucene',
alias: [],
mode: 'lucene'
},
{
name: 'makefile',
label: 'Makefile',
alias: [],
mode: 'makefile'
},
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'mask',
label: 'Mask',
alias: [],
mode: 'mask'
},
{
name: 'matlab',
label: 'MATLAB',
alias: [],
mode: 'matlab'
},
{
name: 'maze',
label: 'Maze',
alias: [],
mode: 'maze'
},
{
name: 'mel',
label: 'MEL',
alias: [],
mode: 'mel'
},
{
name: 'mipsassembler',
label: 'MIPS assembly',
alias: [],
mode: 'mipsassembler'
},
{
name: 'mushcode',
label: 'MUSHCode',
alias: [],
mode: 'mushcode'
},
{
name: 'mysql',
label: 'MySQL',
alias: [],
mode: 'mysql'
},
{
name: 'nix',
label: 'Nix',
alias: [],
mode: 'nix'
},
{
name: 'objectivec',
label: 'Objective C',
alias: ['objc'],
mode: 'objectivec'
},
{
name: 'ocaml',
label: 'OCaml',
alias: [],
mode: 'ocaml'
},
{
name: 'pascal',
label: 'Pascal',
alias: [],
mode: 'pascal'
},
{
name: 'perl',
label: 'Perl',
alias: [],
mode: 'perl'
},
{
name: 'pgsql',
label: 'Postgres SQL',
alias: ['postgres'],
mode: 'pgsql'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'powershell',
label: 'PowerShell',
alias: ['ps1'],
mode: 'powershell'
},
{
name: 'praat',
label: 'Praat',
alias: [],
mode: 'praat'
},
{
name: 'prolog',
label: 'Prolog',
alias: ['pl', 'pro'],
mode: 'prolog'
},
{
name: 'properties',
label: 'Properties',
alias: [],
mode: 'properties'
},
{
name: 'protobuf',
label: 'Protocol Buffers',
alias: ['protocol', 'buffers'],
mode: 'protobuf'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'r',
label: 'R',
alias: ['rlang'],
mode: 'r'
},
{
name: 'rdoc',
label: 'RDoc',
alias: [],
mode: 'rdoc'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'rust',
label: 'Rust',
alias: [],
mode: 'rust'
},
{
name: 'sass',
label: 'Sass',
alias: [],
mode: 'sass'
},
{
name: 'scad',
label: 'SCAD',
alias: [],
mode: 'scad'
},
{
name: 'scala',
label: 'Scala',
alias: [],
mode: 'scala'
},
{
name: 'scheme',
label: 'Scheme',
alias: ['scm', 'ss'],
mode: 'scheme'
},
{
name: 'scss',
label: 'Scss',
alias: [],
mode: 'scss'
},
{
name: 'sh',
label: 'Shell',
alias: ['shell'],
mode: 'sh'
},
{
name: 'sjs',
label: 'StratifiedJS',
alias: ['stratified'],
mode: 'sjs'
},
{
name: 'smarty',
label: 'Smarty',
alias: [],
mode: 'smarty'
},
{
name: 'snippets',
label: 'Snippets',
alias: [],
mode: 'snippets'
},
{
name: 'soy_template',
label: 'Soy Template',
alias: ['soy'],
mode: 'soy_template'
},
{
name: 'space',
label: 'Space',
alias: [],
mode: 'space'
},
{
name: 'sql',
label: 'SQL',
alias: [],
mode: 'sql'
},
{
name: 'sqlserver',
label: 'SQL Server',
alias: [],
mode: 'sqlserver'
},
{
name: 'stylus',
label: 'Stylus',
alias: [],
mode: 'stylus'
},
{
name: 'svg',
label: 'SVG',
alias: [],
mode: 'svg'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'swig',
label: 'SWIG',
alias: [],
mode: 'swig'
},
{
name: 'tcl',
label: 'Tcl',
alias: [],
mode: 'tcl'
},
{
name: 'tex',
label: 'TeX',
alias: [],
mode: 'tex'
},
{
name: 'textile',
label: 'Textile',
alias: [],
mode: 'textile'
},
{
name: 'toml',
label: 'TOML',
alias: [],
mode: 'toml'
},
{
name: 'twig',
label: 'Twig',
alias: [],
mode: 'twig'
},
{
name: 'vala',
label: 'Vala',
alias: [],
mode: 'vala'
},
{
name: 'vbscript',
label: 'VBScript',
alias: ['vbs', 'vbe'],
mode: 'vbscript'
},
{
name: 'velocity',
label: 'Velocity',
alias: [],
mode: 'velocity'
},
{
name: 'verilog',
label: 'Verilog',
alias: [],
mode: 'verilog'
},
{
name: 'vhdl',
label: 'VHDL',
alias: [],
mode: 'vhdl'
},
{
name: 'xml',
label: 'XML',
alias: [],
mode: 'xml'
},
{
name: 'xquery',
label: 'XQuery',
alias: [],
mode: 'xquery'
},
{
name: 'yaml',
label: 'YAML',
alias: [],
mode: 'yaml'
}
]
export default modes

View File

@@ -1,8 +1,5 @@
.root
absolute top bottom right
border-width 1px 0
border-style solid
border-color $ui-borderColor
.empty
height 320px
@@ -15,3 +12,9 @@
line-height 72px
text-align center
color $ui-inactive-text-color
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
.empty-message
color $ui-dark-inactive-text-color

View File

@@ -0,0 +1,9 @@
/**
* Varibales for note detail space.
*/
// Margin on the left side and the right side for NoteDetail component.
$note-detail-left-margin = 25px
$note-detail-right-margin = 25px
$note-detail-box-shadow = 2px 0 15px -8px #b1b1b1 inset

View File

@@ -128,8 +128,8 @@ class FolderSelect extends React.Component {
}
nextOption () {
let { storages } = this.props
let { optionIndex } = this.state
let { folders } = this.props
optionIndex++
if (optionIndex >= folders.length) optionIndex = 0
@@ -184,12 +184,12 @@ class FolderSelect extends React.Component {
}
render () {
let { className, storages, value } = this.props
let { className, data, value } = this.props
let splitted = value.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let options = []
storages.forEach((storage, index) => {
data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => {
options.push({
storage: storage,
@@ -200,6 +200,11 @@ class FolderSelect extends React.Component {
let currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
if (this.state.search.trim().length > 0) {
let filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
options = options.filter((option) => filter.test(option.folder.name))
}
let optionList = options
.map((option, index) => {
return (
@@ -257,13 +262,11 @@ class FolderSelect extends React.Component {
: <div styleName='idle'>
<div styleName='idle-label'>
<span styleName='idle-label-name'
style={{borderColor: currentOption.folder.color}}
style={{color: currentOption.folder.color}}
>
{currentOption.folder.name}
<span styleName='idle-label-name-surfix'>in {currentOption.storage.name}</span>
{currentOption.folder.name} /
</span>
</div>
<i styleName='idle-caret' className='fa fa-fw fa-caret-down'/>
</div>
}

View File

@@ -7,29 +7,28 @@
transition 0.15s
user-select none
&:hover
background-color white
border-color $ui-borderColor
background-color $ui-button--hover-backgroundColor
.root--search, .root--focus
@extend .root
background-color white
background-color $ui-noteDetail-backgroundColor = #F4F4F4
border-color $ui-input--focus-borderColor
width 100px
&:hover
background-color white
border-color $ui-input--focus-borderColor
.idle
position relative
cursor pointer
.idle-label
absolute left top
padding 0 0 0 5px
right 20px
overflow ellipsis
.idle-label-name
border-left solid 4px transparent
padding 2px 5px
font-size 14px
padding 2px
.idle-label-name-surfix
font-size 10px
color $ui-inactive-text-color
@@ -57,8 +56,10 @@
.search-optionList
position fixed
border $ui-border
max-height 450px
overflow auto
z-index 200
border $ui-border
background-color white
border-radius 2px
@@ -80,9 +81,51 @@
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
.search-optionList-item-name
border-left solid 4px transparent
border-left solid 2px transparent
padding 2px 5px
.search-optionList-item-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
body[data-theme="dark"]
.root
color $ui-dark-text-color
&:hover
color white
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-dark-borderColor
.root--search, .root--focus
@extend .root
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-input--focus-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-input--focus-borderColor
.idle-label-name-surfix
color $ui-dark-inactive-text-color
.search-input
color white
background-color transparent
border-color $ui-dark-borderColor
.search-optionList
color white
border-color $ui-dark-borderColor
background-color $ui-dark-button--hover-backgroundColor
.search-optionList-item
&:hover
background-color lighten($ui-dark-button--hover-backgroundColor, 15%)
.search-optionList-item--active
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
.search-optionList-item-name-surfix
color $ui-dark-inactive-text-color

View File

@@ -0,0 +1,27 @@
/**
* @fileoverview Component for show updated date of the detail.
*/
import React, { PropTypes } from 'react'
import { getLastUpdated } from 'browser/lib/date-formatter'
import CSSModules from 'browser/lib/CSSModules'
import styles from './LastUpdatedString.styl'
const LastUpdatedString = ({ date }) => {
let text = ''
try {
text = `Last updated at ${getLastUpdated(date)}`
} catch (e) {
text = ''
}
return (
<p styleName='info-right-date'>{text}</p>
)
}
LastUpdatedString.propTypes = {
date: PropTypes.string
}
export default CSSModules(LastUpdatedString, styles)

View File

@@ -0,0 +1,10 @@
.info-right-date
display inline
line-height 24px
padding-right 25px
font-size 11px
color $ui-button-color
body[data-theme="dark"]
.info-right-date
color $ui-dark-button-color

View File

@@ -8,36 +8,45 @@ import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdown'
import StatusBar from '../StatusBar'
import _ from 'lodash'
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
const { Menu, MenuItem, dialog } = remote
class MarkdownNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
isMovingNote: false,
note: Object.assign({
title: '',
content: '',
isMovingNote: false,
isDeleting: false
}, props.note)
content: ''
}, props.note),
isLockButtonShown: false,
isLocked: false
}
this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
}
focus () {
this.refs.content.focus()
}
componentDidMount () {
ee.on('topbar:togglelockbutton', this.toggleLockButton)
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key && !this.isMovingNote) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note),
isDeleting: false
note: Object.assign({}, nextProps.note)
}, () => {
this.refs.content.reload()
this.refs.tags.reset()
@@ -45,13 +54,24 @@ class MarkdownNoteDetail extends React.Component {
}
}
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
componentDidUnmount () {
ee.off('topbar:togglelockbutton', this.toggleLockButton)
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
let isMarkdownInCode = false
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
if (trimmedLine.match('```')) {
isMarkdownInCode = !isMarkdownInCode
} else if (isMarkdownInCode === false && trimmedLine.match(/^# +/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
@@ -70,6 +90,8 @@ class MarkdownNoteDetail extends React.Component {
}
}
title = markdown.strip(title)
return title
}
@@ -89,15 +111,25 @@ class MarkdownNoteDetail extends React.Component {
}
save () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
saveNow () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
handleFolderChange (e) {
@@ -108,7 +140,7 @@ class MarkdownNoteDetail extends React.Component {
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
@@ -117,13 +149,13 @@ class MarkdownNoteDetail extends React.Component {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
originNote: note,
note: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
key: newNote.storage + '-' + newNote.key
}
})
this.setState({
@@ -149,59 +181,61 @@ class MarkdownNoteDetail extends React.Component {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
handleDeleteButtonClick (e) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a note',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
})
}
handleDeleteConfirmButtonClick (e) {
if (index === 0) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
handleLockButtonMouseDown (e) {
e.preventDefault()
ee.emit('editor:lock')
this.setState({ isLocked: !this.state.isLocked })
if (this.state.isLocked) this.focus()
}
getToggleLockButton () {
return this.state.isLocked ? 'fa-lock' : 'fa-unlock-alt'
}
handleDeleteKeyDown (e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
}
handleToggleLockButton (event, noteStatus) {
// first argument event is not used
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
} else {
this.setState({isLockButtonShown: false})
}
}
handleFocus (e) {
this.focus()
}
render () {
let { storages, config } = this.props
let { data, config } = this.props
let { note } = this.state
return (
@@ -209,58 +243,60 @@ class MarkdownNoteDetail extends React.Component {
style={this.props.style}
styleName='root'
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info'>
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
{(() => {
const faClassName = `fa ${this.getToggleLockButton()}`
const lockButtonComponent =
<button styleName='control-lockButton'
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<i className='fa fa-share-alt fa-fw'/>
<i className={faClassName} styleName='lock-button' />
<span styleName='control-lockButton-tooltip'>
{this.state.isLocked ? 'Unlock' : 'Lock'}
</span>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
)
})()}
<button styleName='control-trashButton'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
<svg height='14px' id='Capa_1' style={{enableBackground: 'new 0 0 753.23 753.23'}} width='14px' version='1.1' viewBox='0 0 753.23 753.23' x='0px' y='0px' xmlSpace='preserve'>
<g>
<g id='_x34__19_'>
<g>
<path d='M494.308,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077c0-12.993-10.545-23.539-23.538-23.539&#xA;&#x9;&#x9;&#x9;&#x9;s-23.538,10.545-23.538,23.539v282.461C470.77,648.531,481.314,659.077,494.308,659.077z M635.538,94.154h-141.23V47.077&#xA;&#x9;&#x9;&#x9;&#x9;C494.308,21.067,473.24,0,447.23,0H306c-26.01,0-47.077,21.067-47.077,47.077v47.077h-141.23&#xA;&#x9;&#x9;&#x9;&#x9;c-26.01,0-47.077,21.067-47.077,47.077v47.077c0,25.986,21.067,47.077,47.077,47.077v423.692&#xA;&#x9;&#x9;&#x9;&#x9;c0,51.996,42.157,94.153,94.154,94.153h329.539c51.996,0,94.153-42.157,94.153-94.153V235.385&#xA;&#x9;&#x9;&#x9;&#x9;c26.01,0,47.077-21.091,47.077-47.077V141.23C682.615,115.221,661.548,94.154,635.538,94.154z M306,70.615&#xA;&#x9;&#x9;&#x9;&#x9;c0-12.993,10.545-23.539,23.538-23.539h94.154c12.993,0,23.538,10.545,23.538,23.539v23.539c-22.809,0-141.23,0-141.23,0V70.615z&#xA;&#x9;&#x9;&#x9;&#x9; M588.461,659.077c0,25.986-21.066,47.076-47.076,47.076H211.846c-26.01,0-47.077-21.09-47.077-47.076V235.385h423.692V659.077z&#xA;&#x9;&#x9;&#x9;&#x9; M612,188.308H141.23c-12.993,0-23.538-10.545-23.538-23.539s10.545-23.539,23.538-23.539H612&#xA;&#x9;&#x9;&#x9;&#x9;c12.993,0,23.538,10.545,23.538,23.539S624.993,188.308,612,188.308z M258.923,659.077c12.993,0,23.539-10.546,23.539-23.539&#xA;&#x9;&#x9;&#x9;&#x9;V353.077c0-12.993-10.545-23.539-23.539-23.539s-23.539,10.545-23.539,23.539v282.461&#xA;&#x9;&#x9;&#x9;&#x9;C235.384,648.531,245.93,659.077,258.923,659.077z M376.615,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077&#xA;&#x9;&#x9;&#x9;&#x9;c0-12.993-10.545-23.539-23.538-23.539s-23.539,10.545-23.539,23.539v282.461C353.077,648.531,363.622,659.077,376.615,659.077z' />
</g>
</g>
</g>
</svg>
</button>
</div>
</div>
}
<div styleName='body'>
<MarkdownEditor
ref='content'
@@ -271,6 +307,11 @@ class MarkdownNoteDetail extends React.Component {
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
date={note.updatedAt}
/>
</div>
)
}

View File

@@ -1,90 +1,57 @@
$info-height = 75px
@import('NoteDetailInfo')
@import('DetailVars')
.root
absolute top bottom right
absolute top right bottom
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
background-color $ui-noteDetail-backgroundColor
box-shadow $note-detail-box-shadow
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.lock-button
padding-bottom 3px
.info-delete
height 80px
clearfix()
.control-lockButton
topBarButtonLight()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
.control-lockButton-tooltip
tooltip()
position fixed
pointer-events none
top 50px
z-index 200
padding 5px
line-height normal
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
opacity 0
transition 0.1s
.info-delete-cancelButton
.control-trashButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
topBarButtonLight()
.body
absolute bottom left right
top $info-height
absolute left right
left $note-detail-left-margin
right $note-detail-right-margin
top $info-height + $info-margin-under-border
bottom $statusBar-height
.body-noteEditor
absolute top bottom left right
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
.control-lockButton
topBarButtonDark()
.control-lockButton-tooltip
darkTooltip()
.control-trashButton
topBarButtonDark()

View File

@@ -0,0 +1,75 @@
@import('DetailVars')
$info-height = 60px
$info-margin-under-border = 27px
.info
absolute top left right
left $note-detail-left-margin
right $note-detail-right-margin
height $info-height
border-bottom $ui-border
background-color $ui-noteDetail-backgroundColor
.info-left
float left
padding 0 5px
margin 0px 2px
.info-left-top
display inline-block
height $info-height
line-height $info-height
.info-left-top-folderSelect
display inline-block
padding 0 3px
height 34px
line-height 34px
vertical-align middle
border-radius 3px
.info-left-button
width 34px
height 34px
navButtonColor()
color $ui-favorite-star-button-color
font-size 14px
margin 13px 2px
padding 0
border-radius 17px
&:hover .info-left-button-tooltip
opacity 1
&:focus
border-color $ui-favorite-star-button-color
&:active, &:active:hover
background-color $ui-favorite-star-button-color
color $ui-button--active-color
.info-right
position absolute
right 0
top 0
background $ui-noteDetail-backgroundColor
bottom 1px
padding-left 30px
body[data-theme="dark"]
.info
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
.info-delete
color $ui-dark-text-color
.info-delete-confirmButton
colorDarkDangerButton()
color $ui-dark-text-color
.info-delete-cancelButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
color $ui-dark-text-color
.info-right
background-color $ui-dark-noteDetail-backgroundColor

View File

@@ -2,40 +2,57 @@ import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetNoteDetail.styl'
import CodeEditor from 'browser/components/CodeEditor'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import modes from 'browser/lib/modes'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror'
import SnippetTab from 'browser/components/SnippetTab'
import StatusBar from '../StatusBar'
import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager'
import _ from 'lodash'
function pass (name) {
switch (name) {
case 'ejs':
return 'Embedded Javascript'
case 'html_ruby':
return 'Embedded Ruby'
case 'objectivec':
return 'Objective C'
case 'text':
return 'Plain Text'
default:
return name
}
}
const electron = require('electron')
const { remote } = electron
const Menu = remote.Menu
const MenuItem = remote.MenuItem
const { Menu, MenuItem, dialog } = remote
class SnippetNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
isMovingNote: false,
snippetIndex: 0,
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
}),
isDeleting: false
})
}
}
focus () {
this.refs.description.focus()
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) {
if (this.saveQueue != null) this.saveNow()
let nextNote = Object.assign({
description: ''
}, nextProps.note, {
@@ -43,8 +60,7 @@ class SnippetNoteDetail extends React.Component {
})
this.setState({
snippetIndex: 0,
note: nextNote,
isDeleting: false
note: nextNote
}, () => {
let { snippets } = this.state.note
snippets.forEach((snippet, index) => {
@@ -55,6 +71,10 @@ class SnippetNoteDetail extends React.Component {
}
}
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
@@ -99,15 +119,25 @@ class SnippetNoteDetail extends React.Component {
}
save () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
dispatch({
type: 'UPDATE_NOTE',
note: this.state.note
})
saveNow () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.folder, note.key, this.state.note)
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
handleFolderChange (e) {
@@ -118,7 +148,7 @@ class SnippetNoteDetail extends React.Component {
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.folder, note.key, newStorageKey, newFolderKey)
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
@@ -127,13 +157,13 @@ class SnippetNoteDetail extends React.Component {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
note: note,
newNote: newNote
originNote: note,
note: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.uniqueKey
key: newNote.storage + '-' + newNote.key
}
})
this.setState({
@@ -159,97 +189,78 @@ class SnippetNoteDetail extends React.Component {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
this.setState({
isDeleting: true
handleDeleteButtonClick (e) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a note',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
})
}
handleDeleteConfirmButtonClick (e) {
if (index === 0) {
let { note, dispatch } = this.props
dataApi
.removeNote(note.storage, note.folder, note.key)
.then(() => {
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'REMOVE_NOTE',
note: note
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
handleDeleteCancelButtonClick (e) {
this.setState({
isDeleting: false
})
}
handleTabPlusButtonClick (e) {
let { note } = this.state
note.snippets = note.snippets.concat([{
name: '',
mode: 'text',
content: ''
}])
this.setState({
note
})
this.addSnippet()
}
handleTabButtonClick (index) {
return (e) => {
handleTabButtonClick (e, index) {
this.setState({
snippetIndex: index
})
}
handleTabDeleteButtonClick (e, index) {
if (this.state.note.snippets.length > 1) {
if (this.state.note.snippets[index].content.trim().length > 0) {
let dialogIndex = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a snippet',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
})
if (dialogIndex === 0) {
this.deleteSnippetByIndex(index)
}
} else {
this.deleteSnippetByIndex(index)
}
}
}
handleTabDeleteButtonClick (index) {
return (e) => {
if (this.state.note.snippets.length > 1) {
let snippets = this.state.note.snippets.slice()
deleteSnippetByIndex (index) {
const snippets = this.state.note.snippets.slice()
snippets.splice(index, 1)
this.state.note.snippets = snippets
this.setState({
note: this.state.note
const note = Object.assign({}, this.state.note, {snippets})
const snippetIndex = this.state.snippetIndex >= snippets.length
? snippets.length - 1
: this.state.snippetIndex
this.setState({ note, snippetIndex }, () => {
this.save()
this.refs['code-' + this.state.snippetIndex].reload()
})
}
}
}
handleNameInputChange (index) {
return (e) => {
renameSnippetByIndex (index, name) {
let snippets = this.state.note.snippets.slice()
snippets[index].name = e.target.value
snippets[index].name = name
let syntax = CodeMirror.findModeByFileName(name.trim())
let mode = syntax != null ? syntax.name : null
if (mode != null) snippets[index].mode = mode
this.state.note.snippets = snippets
this.setState({
@@ -258,20 +269,6 @@ class SnippetNoteDetail extends React.Component {
this.save()
})
}
}
handleModeButtonClick (index) {
return (e) => {
let menu = new Menu()
modes.forEach((mode) => {
menu.append(new MenuItem({
label: mode.label,
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
}))
})
menu.popup(remote.getCurrentWindow())
}
}
handleModeOptionClick (index, name) {
return (e) => {
@@ -300,8 +297,163 @@ class SnippetNoteDetail extends React.Component {
}
}
handleKeyDown (e) {
switch (e.keyCode) {
case 9:
if (e.ctrlKey && !e.shiftKey) {
e.preventDefault()
this.jumpNextTab()
} else if (e.ctrlKey && e.shiftKey) {
e.preventDefault()
this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
e.preventDefault()
this.focusEditor()
}
break
case 76:
{
let isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.focus()
}
}
break
case 84:
{
let isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.addSnippet()
}
}
break
}
}
handleModeButtonClick (e, index) {
let menu = new Menu()
CodeMirror.modeInfo.forEach((mode) => {
menu.append(new MenuItem({
label: mode.name,
click: (e) => this.handleModeOptionClick(index, mode.name)(e)
}))
})
menu.popup(remote.getCurrentWindow())
}
handleIndentTypeButtonClick (e) {
context.popup([
{
label: 'tab',
click: (e) => this.handleIndentTypeItemClick(e, 'tab')
},
{
label: 'space',
click: (e) => this.handleIndentTypeItemClick(e, 'space')
}
])
}
handleIndentSizeButtonClick (e) {
context.popup([
{
label: '2',
click: (e) => this.handleIndentSizeItemClick(e, 2)
},
{
label: '4',
click: (e) => this.handleIndentSizeItemClick(e, 4)
},
{
label: '8',
click: (e) => this.handleIndentSizeItemClick(e, 8)
}
])
}
handleIndentSizeItemClick (e, indentSize) {
let { config, dispatch } = this.props
let editor = Object.assign({}, config.editor, {
indentSize
})
ConfigManager.set({
editor
})
dispatch({
type: 'SET_CONFIG',
config: {
editor
}
})
}
handleIndentTypeItemClick (e, indentType) {
let { config, dispatch } = this.props
let editor = Object.assign({}, config.editor, {
indentType
})
ConfigManager.set({
editor
})
dispatch({
type: 'SET_CONFIG',
config: {
editor
}
})
}
focus () {
this.refs.description.focus()
}
addSnippet () {
let { note } = this.state
note.snippets = note.snippets.concat([{
name: '',
mode: 'Plain Text',
content: ''
}])
let snippetIndex = note.snippets.length - 1
this.setState({
note,
snippetIndex
}, () => {
this.refs['tab-' + snippetIndex].startRenaming()
})
}
jumpNextTab () {
this.setState({
snippetIndex: (this.state.snippetIndex + 1) % this.state.note.snippets.length
}, () => {
this.focusEditor()
})
}
jumpPrevTab () {
this.setState({
snippetIndex: (this.state.snippetIndex - 1 + this.state.note.snippets.length) % this.state.note.snippets.length
}, () => {
this.focusEditor()
})
}
focusEditor () {
console.log('code-' + this.state.snippetIndex)
this.refs['code-' + this.state.snippetIndex].focus()
}
render () {
let { storages, config } = this.props
let { data, config } = this.props
let { note } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10)
@@ -311,58 +463,38 @@ class SnippetNoteDetail extends React.Component {
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <div styleName={isActive
? 'tabList-item--active'
: 'tabList-item'
}
return <SnippetTab
key={index}
>
<button styleName='tabList-item-button'
onClick={(e) => this.handleTabButtonClick(index)(e)}
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='tabList-item-unnamed'>
Unnamed
</span>
}
</button>
{note.snippets.length > 1 &&
<button styleName='tabList-item-deleteButton'
onClick={(e) => this.handleTabDeleteButtonClick(index)(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
ref={'tab-' + index}
snippet={snippet}
isActive={isActive}
onClick={(e) => this.handleTabButtonClick(e, index)}
onDelete={(e) => this.handleTabDeleteButtonClick(e, index)}
onRename={(name) => this.renameSnippetByIndex(index, name)}
isDeletable={note.snippets.length > 1}
/>
})
let viewList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
let mode = snippet.mode === 'text'
? null
: modes.filter((mode) => mode.name === snippet.mode)[0]
let syntax = CodeMirror.findModeByName(pass(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
>
<div styleName='tabView-top'>
<input styleName='tabView-top-name'
placeholder='Filename including extensions...'
value={snippet.name}
onChange={(e) => this.handleNameInputChange(index)(e)}
{snippet.mode === 'Markdown' || snippet.mode === 'GitHub Flavored Markdown'
? <MarkdownEditor styleName='tabView-content'
value={snippet.content}
config={config}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
<button styleName='tabView-top-mode'
onClick={(e) => this.handleModeButtonClick(index)(e)}
>
{mode == null
? 'Select Syntax...'
: mode.label
}&nbsp;
<i className='fa fa-caret-down'/>
</button>
</div>
<CodeEditor styleName='tabView-content'
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
@@ -370,9 +502,11 @@ class SnippetNoteDetail extends React.Component {
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
keyMap={config.editor.keyMap}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
}
</div>
})
@@ -380,63 +514,49 @@ class SnippetNoteDetail extends React.Component {
<div className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
>
{this.state.isDeleting
? <div styleName='info'>
<div styleName='info-delete'>
<span styleName='info-delete-message'>
Are you sure to delete this note?
</span>
<button styleName='info-delete-cancelButton'
onClick={(e) => this.handleDeleteCancelButtonClick(e)}
>Cancel</button>
<button styleName='info-delete-confirmButton'
onClick={(e) => this.handleDeleteConfirmButtonClick(e)}
>Confirm</button>
</div>
</div>
: <div styleName='info'>
<div styleName='info'>
<div styleName='info-left'>
<StarButton styleName='info-left-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
storages={storages}
data={data}
onChange={(e) => this.handleFolderChange(e)}
/>
</div>
<div styleName='info-left-bottom'>
<TagSelect
styleName='info-left-bottom-tagSelect'
ref='tags'
value={this.state.note.tags}
onChange={(e) => this.handleChange(e)}
/>
</div>
</div>
<div styleName='info-right'>
<StarButton styleName='info-right-button'
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
/>
<button styleName='info-right-button'
onClick={(e) => this.handleShareButtonClick(e)}
<button styleName='control-trashButton'
onClick={(e) => this.handleDeleteButtonClick(e)}
>
<i className='fa fa-share-alt fa-fw'/>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
<svg height='14px' id='Capa_1' style={{enableBackground: 'new 0 0 753.23 753.23'}} width='14px' version='1.1' viewBox='0 0 753.23 753.23' x='0px' y='0px' xmlSpace='preserve'>
<g>
<g id='_x34__19_'>
<g>
<path d='M494.308,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077c0-12.993-10.545-23.539-23.538-23.539&#xA;&#x9;&#x9;&#x9;&#x9;s-23.538,10.545-23.538,23.539v282.461C470.77,648.531,481.314,659.077,494.308,659.077z M635.538,94.154h-141.23V47.077&#xA;&#x9;&#x9;&#x9;&#x9;C494.308,21.067,473.24,0,447.23,0H306c-26.01,0-47.077,21.067-47.077,47.077v47.077h-141.23&#xA;&#x9;&#x9;&#x9;&#x9;c-26.01,0-47.077,21.067-47.077,47.077v47.077c0,25.986,21.067,47.077,47.077,47.077v423.692&#xA;&#x9;&#x9;&#x9;&#x9;c0,51.996,42.157,94.153,94.154,94.153h329.539c51.996,0,94.153-42.157,94.153-94.153V235.385&#xA;&#x9;&#x9;&#x9;&#x9;c26.01,0,47.077-21.091,47.077-47.077V141.23C682.615,115.221,661.548,94.154,635.538,94.154z M306,70.615&#xA;&#x9;&#x9;&#x9;&#x9;c0-12.993,10.545-23.539,23.538-23.539h94.154c12.993,0,23.538,10.545,23.538,23.539v23.539c-22.809,0-141.23,0-141.23,0V70.615z&#xA;&#x9;&#x9;&#x9;&#x9; M588.461,659.077c0,25.986-21.066,47.076-47.076,47.076H211.846c-26.01,0-47.077-21.09-47.077-47.076V235.385h423.692V659.077z&#xA;&#x9;&#x9;&#x9;&#x9; M612,188.308H141.23c-12.993,0-23.538-10.545-23.538-23.539s10.545-23.539,23.538-23.539H612&#xA;&#x9;&#x9;&#x9;&#x9;c12.993,0,23.538,10.545,23.538,23.539S624.993,188.308,612,188.308z M258.923,659.077c12.993,0,23.539-10.546,23.539-23.539&#xA;&#x9;&#x9;&#x9;&#x9;V353.077c0-12.993-10.545-23.539-23.539-23.539s-23.539,10.545-23.539,23.539v282.461&#xA;&#x9;&#x9;&#x9;&#x9;C235.384,648.531,245.93,659.077,258.923,659.077z M376.615,659.077c12.993,0,23.538-10.546,23.538-23.539V353.077&#xA;&#x9;&#x9;&#x9;&#x9;c0-12.993-10.545-23.539-23.538-23.539s-23.539,10.545-23.539,23.539v282.461C353.077,648.531,363.622,659.077,376.615,659.077z' />
</g>
</g>
</g>
</svg>
</button>
</div>
</div>
}
<div styleName='body'>
<div styleName='body-description'>
<textarea styleName='body-description-textarea'
<div styleName='description'>
<textarea
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
@@ -448,8 +568,10 @@ class SnippetNoteDetail extends React.Component {
/>
</div>
<div styleName='tabList'>
<div styleName='list'>
{tabList}
<button styleName='tabList-plusButton'
</div>
<button styleName='plusButton'
onClick={(e) => this.handleTabPlusButtonClick(e)}
>
<i className='fa fa-plus' />
@@ -457,6 +579,35 @@ class SnippetNoteDetail extends React.Component {
</div>
{viewList}
</div>
<div styleName='override'>
<button
onClick={(e) => this.handleModeButtonClick(e, this.state.snippetIndex)}
>
{this.state.note.snippets[this.state.snippetIndex].mode == null
? 'Select Syntax...'
: this.state.note.snippets[this.state.snippetIndex].mode
}&nbsp;
<i className='fa fa-caret-down' />
</button>
<button
onClick={(e) => this.handleIndentTypeButtonClick(e)}
>
Indent: {config.editor.indentType}&nbsp;
<i className='fa fa-caret-down' />
</button>
<button
onClick={(e) => this.handleIndentSizeButtonClick(e)}
>
size: {config.editor.indentSize}&nbsp;
<i className='fa fa-caret-down' />
</button>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
date={note.updatedAt}
/>
</div>
)
}

View File

@@ -1,97 +1,28 @@
$info-height = 75px
@import('NoteDetailInfo')
@import('DetailVars')
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-delete
height 80px
clearfix()
.info-delete-message
height 80px
line-height 80px
padding 0 25px
float left
.info-delete-confirmButton
float right
margin 25px 5px 0
height 30px
padding 0 25px
border-radius 2px
border none
color $ui-text-color
colorDangerButton()
.info-delete-cancelButton
float right
height 30px
margin 25px 5px 0
padding 0 25px
border $ui-border
border-radius 2px
color $ui-text-color
colorDefaultButton()
.info-left
float left
padding 0 5px
.info-left-top
height 40px
line-height 40px
.info-left-top-folderSelect
display inline-block
height 34px
width 200px
vertical-align middle
.info-left-bottom
height 30px
.info-left-bottom-tagSelect
height 30px
line-height 30px
.info-right
float right
.info-right-button
width 34px
height 34px
border-radius 17px
navButtonColor()
border $ui-border
font-size 14px
margin 8px 2px
padding 0
&:active
border-color $ui-button--focus-borderColor
&:hover .left-control-newPostButton-tooltip
display block
&:focus
border-color $ui-button--focus-borderColor
background-color $ui-noteDetail-backgroundColor
box-shadow $note-detail-box-shadow
.body
absolute bottom left right
top $info-height
absolute left right
left $note-detail-left-margin
right $note-detail-right-margin
top $info-height + $info-margin-under-border
bottom $statusBar-height
background-color $ui-noteDetail-backgroundColor
.body-description
.body .description
absolute top left right
height 80px
border-bottom $ui-border
.body-description-textarea
.body .description textarea
outline none
display block
height 100%
width 100%
@@ -99,72 +30,75 @@ $info-height = 75px
border none
padding 10px
line-height 1.6
background-color $ui-noteDetail-backgroundColor
.tabList
absolute left right
top 80px
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList-item
position relative
background-color $ui-noteDetail-backgroundColor
.tabList .list
flex 1
border-right $ui-border
&:hover
.tabList-item-deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
.tabList-item--active
@extend .tabList-item
.tabList-item-button
border-color $brand-color
.tabList-item-button
width 100%
height 100%
navButtonColor()
border-left 4px solid transparent
.tabList-item-deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.tabList-plusButton
display flex
overflow hidden
.tabList .plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-top
absolute top left right
height 30px
border-bottom $ui-border
display flex
.tabView-top-name
flex 1
border none
padding 0 10px
font-size 14px
.tabView-top-mode
width 110px
padding 0
border none
border-left $ui-border
colorDefaultButton()
color $ui-inactive-text-color
&:hover
color $ui-text-color
top 130px
.tabView-content
absolute left right bottom
top 30px
absolute top left right bottom
.override
absolute bottom left
left 60px
height 23px
z-index 1
button
navButtonColor()
height 24px
&:active .update-icon
color white
.control-trashButton
float right
topBarButtonLight()
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
.body
background-color $ui-dark-noteDetail-backgroundColor
.body .description textarea
background-color $ui-dark-noteDetail-backgroundColor
color $ui-dark-text-color
.tabList
background-color $ui-button--active-backgroundColor
background-color $ui-dark-noteDetail-backgroundColor
.tabList .list
border-color $ui-dark-borderColor
.tabList .plusButton
navDarkButtonColor()
.override
button
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
color $ui-dark-text-color
.control-trashButton
topBarButtonDark()

View File

@@ -47,7 +47,8 @@ class StarButton extends React.Component {
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}
>
<i className={this.state.isActive || this.props.isActive
<i styleName='icon'
className={this.state.isActive || this.props.isActive
? 'fa fa-star'
: 'fa fa-star-o'
}

View File

@@ -1,11 +1,47 @@
.root
position relative
left 7px
top 0
padding 0
transition transform 0.15s
color alpha($ui-favorite-star-button-color, 60%)
&:hover
transform rotate(-72deg)
transition 0.15s
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-favorite-star-button-color
&:active
transition 0.15s
background-color alpha($ui-button--active-backgroundColor, 40%)
color $ui-favorite-star-button-color
.root--active
@extend .root
color $ui-active-color
transform rotate(-72deg)
color $ui-favorite-star-button-color
&:hover
transition 0.15s
color $ui-favorite-star-button-color
background-color alpha($ui-button--active-backgroundColor, 40%)
&:active
transition 0.15s
color $ui-favorite-star-button-color
background-color alpha($ui-button--active-backgroundColor, 40%)
.icon
transition transform 0.15s
body[data-theme="dark"]
.root
&:hover
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-favorite-star-button-color
&:active
transition 0.15s
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-favorite-star-button-color
.root--active
@extend .root
color $ui-favorite-star-button-color
&:hover
transition 0.15s
color $ui-favorite-star-button-color
background-color alpha($ui-dark-button--active-backgroundColor, 20%)

View File

@@ -22,6 +22,10 @@ class TagSelect extends React.Component {
handleNewTagInputKeyDown (e) {
switch (e.keyCode) {
case 9:
e.preventDefault()
this.submitTag()
break
case 13:
this.submitTag()
break
@@ -53,7 +57,7 @@ class TagSelect extends React.Component {
submitTag () {
let { value } = this.props
let newTag = this.refs.newTag.value.trim()
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
if (newTag.length <= 0) {
this.setState({
@@ -103,12 +107,12 @@ class TagSelect extends React.Component {
<span styleName='tag'
key={tag}
>
<span styleName='tag-label'>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)}
>
<i className='fa fa-times fa-fw'/>
<i className='fa fa-times fa-fw tag-removeButton-icon' />
</button>
<span styleName='tag-label'>{tag}</span>
</span>
)
})
@@ -121,9 +125,6 @@ class TagSelect extends React.Component {
}
styleName='root'
>
<i styleName='icon'
className='fa fa-tags'
/>
{tagList}
<input styleName='newTag'
ref='newTag'
@@ -132,7 +133,6 @@ class TagSelect extends React.Component {
onChange={(e) => this.handleNewTagInputChange(e)}
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
/>
</div>
)
}

View File

@@ -1,46 +1,47 @@
.root
position relative
user-select none
.icon
display inline-block
width 30px
top 19px
user-select none
vertical-align middle
text-align center
color $ui-button-color
width 300px
overflow-x scroll
white-space nowrap
.root::-webkit-scrollbar
display none
.tag
display inline-block
margin 0 2px
margin 1px 3px
padding 0
vertical-align middle
height 20px
background-color white
background-color alpha($ui-tag-backgroundColor, 10%)
border-radius 3px
overflow hidden
clearfix()
.tag-removeButton
float left
float right
height 20px
width 18px
margin 0
padding 0
border-style solid
border-color $ui-borderColor
border-width 0 0 0 3px
border-width 0
border-radius 20px
line-height 18px
background-color transparent
color $ui-button-color
&:hover
background-color $ui-button--hover-backgroundColor
&:active, &:active:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
border-color $ui-button--focus-borderColor
&:focus
border-color $ui-button--focus-borderColor
.tag-removeButton-icon
width 5px
padding-right 4px
.tag-label
font-size 11px
font-weight 600
color: alpha($ui-text-color, 80%)
float left
height 20px
line-height 20px
@@ -53,11 +54,23 @@
height 24px
box-sizing borde-box
border none
border-bottom $ui-border
background-color transparent
outline none
padding 0 4px
&:focus
border-color $ui-input--focus-borderColor = #369DCD
&:disabled
background-color $ui-input--disabled-backgroundColor = #DDD
body[data-theme="dark"]
.tag
background-color alpha($ui-dark-tag-backgroundColor, 60%)
.tag-removeButton
border-color $ui-button--focus-borderColor
background-color transparent
color $ui-button-color
.tag-label
color $ui-dark-text-color
.newTag
border-color none
background-color transparent
color $ui-dark-text-color

View File

@@ -5,6 +5,7 @@ import _ from 'lodash'
import MarkdownNoteDetail from './MarkdownNoteDetail'
import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar'
const OSX = global.process.platform === 'darwin'
@@ -31,19 +32,14 @@ class Detail extends React.Component {
}
render () {
let { location, notes, config } = this.props
let { location, data, config } = this.props
let note = null
if (location.query.key != null) {
let splitted = location.query.key.split('-')
let storageKey = splitted.shift()
let folderKey = splitted.shift()
let noteKey = splitted.shift()
note = _.find(notes, {
storage: storageKey,
folder: folderKey,
key: noteKey
})
note = data.noteMap.get(storageKey + '-' + noteKey)
}
if (note == null) {
@@ -55,6 +51,9 @@ class Detail extends React.Component {
<div styleName='empty'>
<div styleName='empty-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br />to create a new post</div>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
/>
</div>
)
}
@@ -67,7 +66,7 @@ class Detail extends React.Component {
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'data',
'style',
'ignorePreviewPointerEvents',
'location'
@@ -83,7 +82,7 @@ class Detail extends React.Component {
ref='root'
{..._.pick(this.props, [
'dispatch',
'storages',
'data',
'style',
'ignorePreviewPointerEvents',
'location'
@@ -95,7 +94,6 @@ class Detail extends React.Component {
Detail.propTypes = {
dispatch: PropTypes.func,
storages: PropTypes.array,
style: PropTypes.shape({
left: PropTypes.number
}),

View File

@@ -7,11 +7,15 @@ import TopBar from './TopBar'
import NoteList from './NoteList'
import Detail from './Detail'
import dataApi from 'browser/main/lib/dataApi'
import StatusBar from './StatusBar'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import modal from 'browser/main/lib/modal'
import InitModal from 'browser/main/modals/InitModal'
import mixpanel from 'browser/main/lib/mixpanel'
function focused () {
mixpanel.track('MAIN_FOCUSED')
}
class Main extends React.Component {
constructor (props) {
@@ -20,13 +24,30 @@ class Main extends React.Component {
let { config } = props
this.state = {
isSliderFocused: false,
listWidth: config.listWidth
isRightSliderFocused: false,
listWidth: config.listWidth,
navWidth: config.navWidth,
isLeftSliderFocused: false
}
}
getChildContext () {
let { status, config } = this.props
return {
status,
config
}
}
componentDidMount () {
let { dispatch } = this.props
let { dispatch, config } = this.props
if (config.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
// Reload all data
dataApi.init()
@@ -41,19 +62,33 @@ class Main extends React.Component {
modal.open(InitModal)
}
})
window.addEventListener('focus', focused)
}
handleSlideMouseDown (e) {
componentWillUnmount () {
window.removeEventListener('focus', focused)
}
handleLeftSlideMouseDown (e) {
e.preventDefault()
this.setState({
isSliderFocused: true
isLeftSliderFocused: true
})
}
handleRightSlideMouseDown (e) {
e.preventDefault()
this.setState({
isRightSliderFocused: true
})
}
handleMouseUp (e) {
if (this.state.isSliderFocused) {
// Change width of NoteList component.
if (this.state.isRightSliderFocused) {
this.setState({
isSliderFocused: false
isRightSliderFocused: false
}, () => {
let { dispatch } = this.props
let newListWidth = this.state.listWidth
@@ -65,10 +100,26 @@ class Main extends React.Component {
})
})
}
// Change width of SideNav component.
if (this.state.isLeftSliderFocused) {
this.setState({
isLeftSliderFocused: false
}, () => {
let { dispatch } = this.props
let navWidth = this.state.navWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({ navWidth })
dispatch({
type: 'SET_NAV_WIDTH',
navWidth
})
})
}
}
handleMouseMove (e) {
if (this.state.isSliderFocused) {
if (this.state.isRightSliderFocused) {
let offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
@@ -80,6 +131,17 @@ class Main extends React.Component {
listWidth: newListWidth
})
}
if (this.state.isLeftSliderFocused) {
let navWidth = e.pageX
if (navWidth < 80) {
navWidth = 80
} else if (navWidth > 600) {
navWidth = 600
}
this.setState({
navWidth: navWidth
})
}
}
render () {
@@ -95,20 +157,30 @@ class Main extends React.Component {
<SideNav
{..._.pick(this.props, [
'dispatch',
'storages',
'data',
'config',
'location'
])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.navWidth}}
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
draggable='false'
>
<div styleName='slider-hitbox' />
</div>
}
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
ref='body'
style={{left: config.isSideNavFolded ? 44 : this.state.navWidth}}
>
<TopBar style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'config',
'notes',
'data',
'params',
'location'
])}
@@ -116,44 +188,46 @@ class Main extends React.Component {
<NoteList style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'data',
'config',
'params',
'location'
])}
/>
<div styleName={this.state.isSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.listWidth}}
onMouseDown={(e) => this.handleSlideMouseDown(e)}
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
style={{left: this.state.listWidth - 1}}
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
draggable='false'
>
<div styleName='slider-hitbox' />
</div>
<Detail
style={{left: this.state.listWidth + 1}}
style={{left: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'storages',
'notes',
'data',
'config',
'params',
'location'
])}
ignorePreviewPointerEvents={this.state.isSliderFocused}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
/>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
/>
</div>
)
}
}
Main.childContextTypes = {
status: PropTypes.shape({
updateReady: PropTypes.bool.isRequired
}).isRequired,
config: PropTypes.shape({}).isRequired
}
Main.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array
data: PropTypes.shape({}).isRequired
}
export default connect((x) => x)(CSSModules(Main, styles))

View File

@@ -2,8 +2,7 @@
absolute top left bottom right
.body
absolute right top
bottom $statusBar-height - 1
absolute right top bottom
left $sideNav-width
.body--expanded
@@ -12,15 +11,18 @@
.slider
absolute top bottom
top -2px
width 0
.slider-right
@extend .slider
width 1px
background-color $ui-borderColor
border-width 0
border-style solid
border-color $ui-borderColor
.slider--active
@extend .slider
background-color $ui-button--active-backgroundColor
.slider-right--active
@extend .slider-right
.slider-hitbox
absolute top bottom left right
@@ -28,3 +30,12 @@
left -3px
z-index 10
cursor col-resize
body[data-theme="dark"]
.root
absolute top left bottom right
.slider-right
.slider-right--active
box-shadow none

View File

@@ -1,168 +0,0 @@
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

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

View File

@@ -4,6 +4,25 @@ import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import dataApi from 'browser/main/lib/dataApi'
import ConfigManager from 'browser/main/lib/ConfigManager'
import NoteItem from 'browser/components/NoteItem'
import NoteItemSimple from 'browser/components/NoteItemSimple'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt)
}
function sortByAlphabetical (a, b) {
return a.title.localeCompare(b.title)
}
function sortByUpdatedAt (a, b) {
return new Date(b.updatedAt) - new Date(a.updatedAt)
}
class NoteList extends React.Component {
constructor (props) {
@@ -16,12 +35,39 @@ class NoteList extends React.Component {
this.selectPriorNoteHandler = () => {
this.selectPriorNote()
}
this.focusHandler = () => {
this.refs.list.focus()
}
this.alertIfSnippetHandler = () => {
this.alertIfSnippet()
}
this.jumpToTopHandler = () => {
this.jumpToTop()
}
this.state = {
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
ee.on('list:focus', this.focusHandler)
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('list:top', this.jumpToTopHandler)
ee.on('list:jumpToTop', this.jumpToTopHandler)
}
componentWillReceiveProps (nextProps) {
if (nextProps.location.pathname !== this.props.location.pathname) {
this.resetScroll()
}
}
resetScroll () {
this.refs.list.scrollTop = 0
}
componentWillUnmount () {
@@ -29,30 +75,37 @@ class NoteList extends React.Component {
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
ee.off('list:focus', this.focusHandler)
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.off('list:top', this.jumpToTopHandler)
ee.off('list:jumpToTop', this.jumpToTopHandler)
}
componentDidUpdate () {
componentDidUpdate (prevProps) {
let { location } = this.props
if (this.notes.length > 0 && location.query.key == null) {
let { router } = this.context
router.replace({
pathname: location.pathname,
query: {
key: this.notes[0].uniqueKey
key: this.notes[0].storage + '-' + this.notes[0].key
}
})
return
}
// Auto scroll
if (_.isString(location.query.key)) {
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
return note != null && note.storage + '-' + note.key === location.query.key
})
if (targetIndex > -1) {
let list = this.refs.root
let list = this.refs.list
let item = list.childNodes[targetIndex]
if (item == null) return false
let overflowBelow = item.offsetTop + item.clientHeight - list.clientHeight - list.scrollTop > 0
if (overflowBelow) {
list.scrollTop = item.offsetTop + item.clientHeight - list.clientHeight
@@ -73,7 +126,7 @@ class NoteList extends React.Component {
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
return note.storage + '-' + note.key === location.query.key
})
if (targetIndex === 0) {
@@ -85,7 +138,7 @@ class NoteList extends React.Component {
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
}
@@ -98,20 +151,21 @@ class NoteList extends React.Component {
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.uniqueKey === location.query.key
return note.storage + '-' + note.key === location.query.key
})
if (targetIndex === this.notes.length - 1) {
return
}
targetIndex = 0
} else {
targetIndex++
if (targetIndex < 0) targetIndex = 0
else if (targetIndex > this.notes.length - 1) targetIndex === this.notes.length - 1
}
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].uniqueKey
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
ee.emit('list:moved')
@@ -120,34 +174,20 @@ class NoteList extends React.Component {
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
// if (e.keyCode === 65 && !e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('top-new-post')
// }
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
ee.emit('top:new-note')
}
// if (e.keyCode === 65 && e.shiftKey) {
// e.preventDefault()
// remote.getCurrentWebContents().send('nav-new-folder')
// }
if (e.keyCode === 68) {
e.preventDefault()
ee.emit('detail:delete')
}
// if (e.keyCode === 68) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-delete')
// }
// if (e.keyCode === 84) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-title')
// }
// if (e.keyCode === 69) {
// e.preventDefault()
// }
// if (e.keyCode === 83) {
// e.preventDefault()
// remote.getCurrentWebContents().send('detail-save')
// }
if (e.keyCode === 69) {
e.preventDefault()
ee.emit('detail:focus')
}
if (e.keyCode === 38) {
e.preventDefault()
@@ -161,34 +201,41 @@ class NoteList extends React.Component {
}
getNotes () {
let { storages, notes, params, location } = this.props
let { data, params, location } = this.props
if (location.pathname.match(/\/home/)) {
return notes
return data.noteMap.map((note) => note)
}
if (location.pathname.match(/\/starred/)) {
return notes
.filter((note) => note.isStarred)
return data.starredSet.toJS()
.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
let storageKey = params.storageKey
let folderKey = params.folderKey
let storage = _.find(storages, {key: storageKey})
let storage = data.storageMap.get(storageKey)
if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) {
return notes
.filter((note) => note.storage === storageKey)
let storageNoteSet = data.storageNoteMap
.get(storage.key)
if (storageNoteSet == null) storageNoteSet = []
return storageNoteSet
.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
return notes
.filter((note) => note.folder === folderKey)
let folderNoteKeyList = data.folderNoteMap
.get(storage.key + '-' + folder.key)
return folderNoteKeyList != null
? folderNoteKeyList
.map((uniqueKey) => data.noteMap.get(uniqueKey))
: []
}
handleNoteClick (uniqueKey) {
return (e) => {
handleNoteClick (e, uniqueKey) {
let { router } = this.context
let { location } = this.props
@@ -199,88 +246,199 @@ class NoteList extends React.Component {
}
})
}
handleNoteContextMenu (e, uniqueKey) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete Note',
click: (e) => this.handleDeleteNote(e, uniqueKey)
}))
menu.popup()
}
handleDeleteNote (e, uniqueKey) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete a note',
detail: 'This work cannot be undone.',
buttons: ['Confirm', 'Cancel']
})
if (index === 0) {
let { dispatch, location } = this.props
let splitted = uniqueKey.split('-')
let storageKey = splitted.shift()
let noteKey = splitted.shift()
dataApi
.deleteNote(storageKey, noteKey)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
if (location.query.key === uniqueKey) {
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
} else {
dispatchHandler()
}
})
}
}
handleSortByChange (e) {
let { dispatch } = this.props
let config = {
sortBy: e.target.value
}
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
}
handleListStyleButtonClick (e, style) {
let { dispatch } = this.props
let config = {
listStyle: style
}
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
}
alertIfSnippet () {
let { location } = this.props
const targetIndex = _.findIndex(this.notes, (note) => {
return `${note.storage}-${note.key}` === location.query.key
})
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Sorry!',
detail: 'md/text import is available only a markdown note.'
})
}
}
jumpToTop () {
if (this.notes === null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
const targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
}
render () {
let { location, storages, notes } = this.props
let { location, notes, config } = this.props
let sortFunc = config.sortBy === 'CREATED_AT'
? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL'
? sortByAlphabetical
: sortByUpdatedAt
this.notes = notes = this.getNotes()
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt))
.sort(sortFunc)
let noteList = notes
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let folder = _.find(storage.folders, {key: note.folder})
let tagElements = _.isArray(note.tags)
? note.tags.map((tag) => {
.map(note => {
if (note == null) {
return null
}
const isDefault = config.listStyle === 'DEFAULT'
const isActive = location.query.key === note.storage + '-' + note.key
const dateDisplay = moment(
config.sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt
).fromNow()
const key = `${note.storage}-${note.key}`
if (isDefault) {
return (
<span styleName='item-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
let isActive = location.query.key === note.uniqueKey
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={note.uniqueKey}
onClick={(e) => this.handleNoteClick(note.uniqueKey)(e)}
>
<div styleName='item-border'/>
<div styleName='item-info'>
<div styleName='item-info-left'>
<span styleName='item-info-left-folder'
style={{borderColor: folder.color}}
>
{folder.name}
<span styleName='item-info-left-folder-surfix'>in {storage.name}</span>
</span>
</div>
<div styleName='item-info-right'>
{moment(note.updatedAt).fromNow()}
</div>
</div>
<div styleName='item-title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='item-title-icon' className='fa fa-fw fa-code'/>
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o'/>
}
{note.title.trim().length > 0
? note.title
: <span styleName='item-title-empty'>Empty</span>
}
</div>
<div styleName='item-tagList'>
<i styleName='item-tagList-icon'
className='fa fa-tags fa-fw'
<NoteItem
isActive={isActive}
note={note}
dateDisplay={dateDisplay}
key={key}
handleNoteClick={this.handleNoteClick.bind(this)}
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
/>
{tagElements.length > 0
? tagElements
: <span styleName='item-tagList-empty'>Not tagged yet</span>
)
}
</div>
</div>
return (
<NoteItemSimple
isActive={isActive}
note={note}
key={key}
handleNoteClick={this.handleNoteClick.bind(this)}
handleNoteContextMenu={this.handleNoteContextMenu.bind(this)}
/>
)
})
return (
<div className='NoteList'
styleName='root'
ref='root'
tabIndex='0'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
style={this.props.style}
>
<div styleName='control'>
<div styleName='control-sortBy'>
<i className='fa fa-bolt' />
<select styleName='control-sortBy-select'
value={config.sortBy}
onChange={(e) => this.handleSortByChange(e)}
>
<option value='UPDATED_AT'>Updated Time</option>
<option value='CREATED_AT'>Created Time</option>
<option value='ALPHABETICAL'>Alphabetical</option>
</select>
</div>
<button styleName={config.listStyle === 'DEFAULT'
? 'control-button--active'
: 'control-button'
}
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
>
<i className='fa fa-th-large' />
</button>
<button styleName={config.listStyle === 'SMALL'
? 'control-button--active'
: 'control-button'
}
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
>
<i className='fa fa-list-ul' />
</button>
</div>
<div styleName='list'
ref='list'
tabIndex='-1'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
>
{noteList}
</div>
</div>
)
}
}

View File

@@ -1,53 +1,34 @@
.root
absolute top left
bottom $statusBar-height - 1
absolute top left bottom
width $sideNav-width
border-right $ui-border
border-bottom $ui-border
background-color $ui-backgroundColor
user-select none
color $ui-text-color
.top
height $topBar-height
border-bottom $ui-border
.top-menu
navButtonColor()
height $topBar-height - 1
padding 0 10px
font-size 14px
height $topBar-height
padding 0 15px
font-size 12px
width 100%
text-align left
&:hover
color $ui-text-color
&:active, &:active:hover
color $ui-text-color
background-color alpha($ui-button--active-backgroundColor, 20%)
.top-menu-label
margin-left 5px
.menu
margin-top 15px
.menu-button
navButtonColor()
height 44px
padding 0 10px
font-size 14px
width 100%
text-align left
.menu-button--active
@extend .menu-button
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.menu-button-label
margin-left 5px
overflow ellipsis
.storageList
absolute left right
bottom 44px
top 178px
bottom 37px
top 160px
overflow-y auto
.storageList-empty
@@ -78,45 +59,72 @@
width 44px - 1
text-align center
&:hover .top-menu-label
width 100px
transition opacity 0.15s
opacity 1
.top-menu-label
position fixed
display inline-block
height 34px
left 44px
width 0
margin-top -5px
height 30px
left 32px
padding 0 10px
margin-top -8px
opacity 0
margin-left 0
overflow hidden
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
line-height 30px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
font-size 12px
.menu-button, .menu-button--active
width 44px - 1
text-align center
&:hover .menu-button-label
width 100px
// TODO: extract tooltip style to a mixin
transition opacity 0.15s
opacity 1
.menu-button-label
position fixed
display inline-block
height 34px
height 32px
left 44px
width 0
padding-left 0
margin-top -9px
padding 0 10px
margin-top -8px
margin-left 0
overflow ellipsis
background-color $ui-tooltip-backgroundColor
z-index 10
color white
line-height 34px
border-top-right-radius 5px
border-bottom-right-radius 5px
transition width 0.15s
line-height 32px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
opacity 0
font-size 12px
body[data-theme="dark"]
.root, .root--folded
border-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
color $ui-dark-text-color
.top
border-color $ui-dark-borderColor
.top-menu
navDarkButtonColor()
&:active
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.storageList-empty
color $ui-dark-inactive-text-color
.navToggle
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
color $ui-dark-text-color

View File

@@ -2,6 +2,14 @@ import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
import modal from 'browser/main/lib/modal'
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
class StorageItem extends React.Component {
constructor (props) {
@@ -12,12 +20,59 @@ class StorageItem extends React.Component {
}
}
handleHeaderContextMenu (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Add Folder',
click: (e) => this.handleAddFolderButtonClick(e)
}))
menu.append(new MenuItem({
type: 'separator'
}))
menu.append(new MenuItem({
label: 'Unlink Storage',
click: (e) => this.handleUnlinkStorageClick(e)
}))
menu.popup()
}
handleUnlinkStorageClick (e) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Unlink Storage',
detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)',
buttons: ['Confirm', 'Cancel']
})
if (index === 0) {
let { storage, dispatch } = this.props
dataApi.removeStorage(storage.key)
.then(() => {
dispatch({
type: 'REMOVE_STORAGE',
storageKey: storage.key
})
})
.catch((err) => {
throw err
})
}
}
handleToggleButtonClick (e) {
this.setState({
isOpen: !this.state.isOpen
})
}
handleAddFolderButtonClick (e) {
let { storage } = this.props
modal.open(CreateFolderModal, {
storage
})
}
handleHeaderInfoClick (e) {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key)
@@ -30,35 +85,88 @@ class StorageItem extends React.Component {
}
}
render () {
let { storage, location } = this.props
let folderList = storage.folders.map((folder) => {
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))
return <button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
handleFolderButtonContextMenu (e, folder) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Rename Folder',
click: (e) => this.handleRenameFolderClick(e, folder)
}))
menu.append(new MenuItem({
type: 'separator'
}))
menu.append(new MenuItem({
label: 'Delete Folder',
click: (e) => this.handleFolderDeleteClick(e, folder)
}))
menu.popup()
}
handleRenameFolderClick (e, folder) {
let { storage } = this.props
modal.open(RenameFolderModal, {
storage,
folder
})
}
handleFolderDeleteClick (e, folder) {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Delete Folder',
detail: 'This work will deletes all notes in the folder and can not be undone.',
buttons: ['Confirm', 'Cancel']
})
if (index === 0) {
let { storage, dispatch } = this.props
dataApi
.deleteFolder(storage.key, folder.key)
.then((data) => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
}
render () {
let { storage, location, isFolded, data } = this.props
let { folderNoteMap } = data
let folderList = storage.folders.map((folder) => {
let isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)))
let noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = noteSet != null
? noteSet.size
: 0
return (
<StorageItemChild
key={folder.key}
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
>
<span styleName='folderList-item-name'
style={{borderColor: folder.color}}
>
{folder.name}
</span>
</button>
isActive={isActive}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
/>
)
})
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
return (
<div styleName='root'
<div styleName={isFolded ? 'root--folded' : 'root'}
key={storage.key}
>
<div styleName={isActive
? 'header--active'
: 'header'
}>
}
onContextMenu={(e) => this.handleHeaderContextMenu(e)}
>
<button styleName='header-toggleButton'
onMouseDown={(e) => this.handleToggleButtonClick(e)}
>
@@ -68,15 +176,26 @@ class StorageItem extends React.Component {
}
/>
</button>
{!isFolded &&
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
>
<i className='fa fa-plus' />
</button>
}
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
{isFolded ? storage.name.substring(0, 1) : storage.name}
</span>
{isFolded &&
<span styleName='header-info--folded-tooltip'>
{storage.name}
</span>
<span styleName='header-info-path'>
({storage.path})
</span>
}
</button>
</div>
{this.state.isOpen &&
@@ -90,6 +209,7 @@ class StorageItem extends React.Component {
}
StorageItem.propTypes = {
isFolded: PropTypes.bool
}
export default CSSModules(StorageItem, styles)

View File

@@ -1,89 +1,132 @@
.root
width 100%
user-select none
.header
position relative
height 30px
height 26px
width 100%
&:hover
background-color $ui-button--hover-backgroundColor
&:active
.header-toggleButton
color white
margin-bottom 5px
transition 0.15s
.header--active
@extend .header
.header-info
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
transition color background-color 0.15s
.header--active
.header-toggleButton
color white
&:active
color white
color $ui-text-color
.header--active
.header-info
color $ui-text-color
.header--active
.header-addFolderButton
color $ui-text-color
.header-toggleButton
navButtonColor()
position absolute
left 0
width 25px
height 30px
height 26px
padding 0
border none
color $ui-inactive-text-color
background-color transparent
&:hover
background-color transparent
color $ui-text-color
&:active
color $ui-active-color
.header-info
navButtonColor()
display block
width 100%
height 30px
padding-left 25px
padding-right 10px
line-height 30px
line-height 26px
cursor pointer
font-size 14px
font-size 13px
border none
overflow ellipsis
text-align left
background-color transparent
color $ui-inactive-text-color
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-info-path
font-size 10px
margin 0 5px
.folderList-item
display block
width 100%
height 3 0px
background-color transparent
color $ui-inactive-text-color
.header-addFolderButton
navButtonColor()
position absolute
right 0
width 25px
height 26px
padding 0
margin 2px 0
text-align left
border none
font-size 14px
&:hover
background-color $ui-button--hover-backgroundColor
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
&:hover
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.folderList-item-name
display block
background-color transparent
color $ui-text-color
.root--folded
@extend .root
.header
width 100%
.header-info
overflow ellipsis
padding 0 0 0 18px
&:hover .header-info--folded-tooltip
opacity 1
.header-info-path
display none
.header-toggleButton
width 15px
.header-info--folded-tooltip
tooltip()
position fixed
padding 0 10px
height 30px
line-height 30px
border-width 0 0 0 4px
border-style solid
border-color transparent
left 44px
z-index 10
pointer-events none
opacity 0
border-top-right-radius 2px
border-bottom-right-radius 2px
.header-info--folded-tooltip-path
font-size 10px
margin 0 5px
body[data-theme="dark"]
.header--active
background-color $ui-dark-button--active-backgroundColor
transition color background-color 0.15s
.header--active
.header-toggleButton
color $ui-dark-text-color
.header--active
.header-info
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.header--active
.header-addFolderButton
color $ui-dark-text-color
.header-toggleButton
&:hover
color $ui-dark-text-color
.header-info
&:hover
color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
&:active, &:active:hover
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.header-addFolderButton
&:hover
color $ui-dark-text-color

View File

@@ -5,9 +5,7 @@ import { openModal } from 'browser/main/lib/modal'
import PreferencesModal from '../modals/PreferencesModal'
import ConfigManager from 'browser/main/lib/ConfigManager'
import StorageItem from './StorageItem'
const electron = require('electron')
const { remote } = electron
import SideNavFilter from 'browser/components/SideNavFilter'
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
@@ -36,23 +34,29 @@ class SideNav extends React.Component {
}
render () {
let { storages, location, config } = this.props
let { data, location, config, dispatch } = this.props
let isFolded = config.isSideNavFolded
let isHomeActive = location.pathname.match(/^\/home$/)
let isStarredActive = location.pathname.match(/^\/starred$/)
let storageList = storages.map((storage) => {
let isHomeActive = !!location.pathname.match(/^\/home$/)
let isStarredActive = !!location.pathname.match(/^\/starred$/)
let storageList = data.storageMap.map((storage, key) => {
return <StorageItem
key={storage.key}
storage={storage}
data={data}
location={location}
isFolded={isFolded}
dispatch={dispatch}
/>
})
let style = {}
if (!isFolded) style.width = this.props.width
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
tabIndex='1'
style={style}
>
<div styleName='top'>
<button styleName='top-menu'
@@ -63,27 +67,19 @@ class SideNav extends React.Component {
</button>
</div>
<div styleName='menu'>
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleHomeButtonClick(e)}
>
<i className='fa fa-home fa-fw'/>
<span styleName='menu-button-label'>Home</span>
</button>
<button styleName={isStarredActive ? 'menu-button--active' : 'menu-button'}
onClick={(e) => this.handleStarredButtonClick(e)}
>
<i className='fa fa-star fa-fw'/>
<span styleName='menu-button-label'>Starred</span>
</button>
</div>
<SideNavFilter
isFolded={isFolded}
isHomeActive={isHomeActive}
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
isStarredActive={isStarredActive}
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
/>
<div styleName='storageList'>
{storageList.length > 0 ? storageList : (
<div styleName='storageList-empty'>No storage mount.</div>
)}
</div>
{false &&
<button styleName='navToggle'
onClick={(e) => this.handleToggleButtonClick(e)}
>
@@ -92,7 +88,6 @@ class SideNav extends React.Component {
: <i className='fa fa-angle-double-left' />
}
</button>
}
</div>
)
}

View File

@@ -1,33 +1,31 @@
@import('../Detail/DetailVars')
.root
absolute bottom left right
height $statusBar-height - 1
background-color $ui-backgroundColor
height $statusBar-height
background-color $ui-noteDetail-backgroundColor
display flex
.pathname
absolute left
.blank
flex 1
.help
navButtonColor()
height 24px
overflow ellipsis
right 185px
line-height 24px
font-size 12px
padding 0 15px
color $ui-inactive-text-color
width 24px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
.zoom
navButtonColor()
absolute right
height 24px
width 60px
border-width 0 1px
border-style solid
border-color $ui-borderColor
.update
navButtonColor()
position absolute
right 60px
height 24px
width 125px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
@@ -36,3 +34,26 @@
.update-icon
color $brand-color
body[data-theme="dark"]
.root
background-color $ui-dark-noteDetail-backgroundColor
border-color $ui-dark-borderColor
box-shadow none
.zoom
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
color $ui-dark-text-color
.help
navButtonColor()
border-color $ui-dark-borderColor
border-left 1px solid $ui-dark-borderColor
.update
navDarkButtonColor()
border-color $ui-dark-borderColor
border-left 1px solid $ui-dark-borderColor

View File

@@ -2,29 +2,15 @@ import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
import LastUpdatedString from '../Detail/LastUpdatedString'
const electron = require('electron')
const ipc = electron.ipcRenderer
const { remote } = electron
const { remote, ipcRenderer } = electron
const { Menu, MenuItem, dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3]
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
class StatusBar extends React.Component {
constructor (props) {
super(props)
this.state = {
updateAvailable: false
}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
@@ -34,7 +20,7 @@ class StatusBar extends React.Component {
})
if (index === 0) {
ipc.send('update-app', 'Deal with it.')
ipcRenderer.send('update-app-confirm')
}
}
@@ -61,30 +47,42 @@ class StatusBar extends React.Component {
}
render () {
let { config, location } = this.props
let { config, status } = this.context
return (
<div className='StatusBar'
styleName='root'
>
<div styleName='pathname'>{location.pathname + location.search}</div>
{this.state.updateAvailable
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download'/> Update is available!
</button>
: null
}
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<i className='fa fa-search-plus' />&nbsp;
{Math.floor(config.zoom * 100)}%
</button>
<div styleName='blank' />
{status.updateReady
? <button onClick={this.updateApp} styleName='update'>
<i styleName='update-icon' className='fa fa-cloud-download' /> Ready to Update!
</button>
: null
}
<LastUpdatedString date={this.props.date} />
</div>
)
}
}
StatusBar.contextTypes = {
status: PropTypes.shape({
updateReady: PropTypes.bool.isRequired
}).isRequired,
config: PropTypes.shape({}).isRequired,
date: PropTypes.string
}
StatusBar.propTypes = {
config: PropTypes.shape({
zoom: PropTypes.number

View File

@@ -1,30 +1,34 @@
.root
position relative
width 100%
background-color $ui-backgroundColor
background-color $ui-noteList-backgroundColor
height $topBar-height - 1
.root--expanded
@extend .root
$control-height = 34px
.control
position absolute
top 8px
top 13px
left 8px
right 8px
height $control-height
border $ui-border
border-radius 20px
overflow hidden
display flex
.control-search
absolute top left bottom
right 40px
height 32px
flex 1
background-color white
position relative
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-inactive-text-color
background-color $ui-noteList-backgroundColor
.control-search-input
display block
@@ -35,16 +39,18 @@ $control-height = 34px
height 100%
outline none
border none
background-color $ui-noteList-backgroundColor
.control-search-optionList
position fixed
z-index 200
width 275px
height 175px
width 500px
height 250px
overflow-y auto
background-color $modal-background
border-radius 2px
box-shadow 2px 2px 10px gray
border-none
box-shadow 0 0 1px rgba(76,86,103,.25), 0 2px 18px rgba(31,37,50,.32)
.control-search-optionList-item
height 50px
@@ -54,15 +60,94 @@ $control-height = 34px
cursor pointer
overflow ellipsis
&:hover
background-color alpha($ui-active-color, 10%)
background-color alpha(#D4D4D4, 30%)
.control-search-optionList-item-folder
border-left 4px solid transparent
border-left 2px solid transparent
padding 2px 5px
color $ui-text-color
overflow ellipsis
font-size 12px
height 16px
margin-bottom 4px
.control-search-optionList-item-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.control-search-optionList-item-type
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.control-search-optionList-empty
height 150px
color $ui-inactive-text-color
line-height 150px
text-align center
.control-newPostButton
display block
width 32px
height $control-height - 2
navButtonColor()
border $ui-border
border-radius 32px
font-size 14px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .control-newPostButton-tooltip
opacity 1
.control-newPostButton-tooltip
tooltip()
position fixed
pointer-events none
top 50px
left 433px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
body[data-theme="dark"]
.root, .root--expanded
background-color $ui-dark-noteList-backgroundColor
.control
border-color $ui-dark-borderColor
.control-search
background-color $dark-background-color
.control-search-icon
absolute top bottom left
line-height 32px
width 35px
color $ui-dark-inactive-text-color
background-color $ui-dark-noteList-backgroundColor
.control-search-input
input
background-color $ui-dark-noteList-backgroundColor
color $ui-dark-text-color
.control-search-optionList
color white
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-dark-borderColor
box-shadow 2px 2px 10px black
.control-search-optionList-item
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.control-search-optionList-item-folder
color $ui-dark-text-color
.control-search-optionList-item-folder-surfix
font-size 10px
margin-left 5px
@@ -72,35 +157,18 @@ $control-height = 34px
color $ui-inactive-text-color
padding-right 3px
.control-search-optionList-empty
height 150px
color $ui-inactive-text-color
line-height 150px
text-align center
.control-newPostButton
display block
absolute top right bottom
width 40px
height $control-height - 2
navButtonColor()
border-left $ui-border
font-size 14px
line-height 28px
padding 0
color $ui-inactive-text-color
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
&:hover
transition 0.15s
color $ui-dark-text-color
&:active
border-color $ui-button--active-backgroundColor
&:hover .left-control-newPostButton-tooltip
display block
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
border-color $ui-dark-button--active-backgroundColor
.control-newPostButton-tooltip
position fixed
line-height 1.4
background-color $ui-tooltip-backgroundColor
color $ui-tooltip-text-color
font-size 10px
margin-left -25px
margin-top 5px
padding 5px
z-index 1
border-radius 5px
display none
pointer-events none
darkTooltip()

View File

@@ -6,6 +6,8 @@ import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import ConfigManager from 'browser/main/lib/ConfigManager'
import dataApi from 'browser/main/lib/dataApi'
const OSX = window.process.platform === 'darwin'
@@ -22,24 +24,36 @@ class TopBar extends React.Component {
this.newNoteHandler = () => {
this.handleNewPostButtonClick()
}
this.focusSearchHandler = () => {
this.handleOnSearchFocus()
}
}
componentDidMount () {
ee.on('top:new-note', this.newNoteHandler)
ee.on('top:focus-search', this.focusSearchHandler)
}
componentWillUnmount () {
ee.off('top:new-note', this.newNoteHandler)
ee.off('top:focus-search', this.focusSearchHandler)
}
handleNewPostButtonClick (e) {
let { storages, params, dispatch, location } = this.props
let storage = _.find(storages, {key: params.storageKey})
if (storage == null) storage = storages[0]
if (storage == null) throw new Error('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0]
if (folder == null) throw new Error('No folder to craete a note')
let { config } = this.props
switch (config.ui.defaultNote) {
case 'MARKDOWN_NOTE':
this.createNote('MARKDOWN_NOTE')
break
case 'SNIPPET_NOTE':
this.createNote('SNIPPET_NOTE')
break
case 'ALWAYS_ASK':
default:
let { dispatch, location } = this.props
let { storage, folder } = this.resolveTargetFolder()
modal.open(NewNoteModal, {
storage: storage.key,
@@ -48,6 +62,29 @@ class TopBar extends React.Component {
location
})
}
}
resolveTargetFolder () {
let { data, params } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (let kv of data.storageMap) {
storage = kv[1]
break
}
}
if (storage == null) window.alert('No storage to create a note')
let folder = _.find(storage.folders, {key: params.folderKey})
if (folder == null) folder = storage.folders[0]
if (folder == null) window.alert('No folder to create a note')
return {
storage,
folder
}
}
handleSearchChange (e) {
this.setState({
@@ -56,26 +93,60 @@ class TopBar extends React.Component {
}
getOptions () {
let { notes } = this.props
let { data } = this.props
let { search } = this.state
let notes = data.noteMap.map((note) => note)
if (search.trim().length === 0) return []
let searchBlocks = search.split(' ')
searchBlocks.forEach((block) => {
if (block.match(/^#.+/)) {
let tag = block.match(/#(.+)/)[1]
if (block.match(/^!#.+/)) {
let tag = block.match(/^!#(.+)/)[1]
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
notes = notes
.filter((note) => {
if (!_.isArray(note.tags)) return false
return note.tags.some((_tag) => {
return _tag === tag
return _tag.match(regExp)
})
})
} else if (block.match(/^!.+/)) {
let block = block.match(/^!(.+)/)[1]
let regExp = new RegExp(_.escapeRegExp(block), 'i')
notes = notes.filter((note) => {
if (!_.isArray(note.tags) || !note.tags.some((_tag) => {
return _tag.match(regExp)
})) {
return true
}
if (note.type === 'SNIPPET_NOTE') {
return !note.description.match(regExp)
} else if (note.type === 'MARKDOWN_NOTE') {
return !note.content.match(regExp)
}
return false
})
} else if (block.match(/^#.+/)) {
let tag = block.match(/#(.+)/)[1]
let regExp = new RegExp(_.escapeRegExp(tag), 'i')
notes = notes
.filter((note) => {
if (!_.isArray(note.tags)) return false
return note.tags.some((_tag) => {
return _tag.match(regExp)
})
})
} else {
let regExp = new RegExp(_.escapeRegExp(block), 'i')
notes = notes.filter((note) => {
if (_.isArray(note.tags) && note.tags.some((_tag) => {
return _tag.match(regExp)
})) {
return true
}
if (note.type === 'SNIPPET_NOTE') {
return note.description.match(block)
return note.description.match(regExp)
} else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(block)
return note.content.match(regExp)
}
return false
})
@@ -125,15 +196,77 @@ class TopBar extends React.Component {
}
}
createNote (noteType) {
let { dispatch, location } = this.props
if (noteType !== 'MARKDOWN_NOTE' && noteType !== 'SNIPPET_NOTE') throw new Error('Invalid note type.')
let { storage, folder } = this.resolveTargetFolder()
let newNote = noteType === 'MARKDOWN_NOTE'
? {
type: 'MARKDOWN_NOTE',
folder: folder.key,
title: '',
content: ''
}
: {
type: 'SNIPPET_NOTE',
folder: folder.key,
title: '',
description: '',
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}
dataApi
.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: note.storage + '-' + note.key}
})
ee.emit('detail:focus')
})
}
setDefaultNote (defaultNote) {
let { config, dispatch } = this.props
let ui = Object.assign(config.ui)
ui.defaultNote = defaultNote
ConfigManager.set({
ui
})
dispatch({
type: 'SET_UI',
config: ConfigManager.get()
})
}
handleOnSearchFocus () {
if (this.state.searchPopupOpen) {
this.refs.search.childNodes[0].blur()
} else {
this.refs.search.childNodes[0].focus()
}
}
render () {
let { config, style, storages } = this.props
let { config, style, data } = this.props
let searchOptionList = this.getOptions()
.map((note) => {
let storage = _.find(storages, {key: note.storage})
let storage = data.storageMap.get(note.storage)
let folder = _.find(storage.folders, {key: note.folder})
return <div styleName='control-search-optionList-item'
key={note.uniqueKey}
onClick={(e) => this.handleOptionClick(note.uniqueKey)(e)}
key={note.storage + '-' + note.key}
onClick={(e) => this.handleOptionClick(note.storage + '-' + note.key)(e)}
>
<div styleName='control-search-optionList-item-folder'
style={{borderColor: folder.color}}>
@@ -191,7 +324,7 @@ class TopBar extends React.Component {
onClick={(e) => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus' />
<span styleName='control-newPostButton-tooltip'>
New Note {OSX ? '⌘' : '^'} + n
Make a Note {OSX ? '⌘' : '^'} + n
</span>
</button>
</div>

View File

@@ -1,6 +1,6 @@
global-reset()
DEFAULT_FONTS = 'Lato', helvetica, arial, sans-serif
DEFAULT_FONTS = 'OpenSans', helvetica, arial, sans-serif
html, body
width 100%
@@ -11,7 +11,7 @@ body
font-family DEFAULT_FONTS
color textColor
font-size fontSize
font-weight 400
font-weight 200
button, input, select, textarea
font-family DEFAULT_FONTS
@@ -83,3 +83,22 @@ modalBackColor = transparentify(white, 65%)
absolute top left bottom right
background-color modalBackColor
z-index modalZIndex + 1
body[data-theme="dark"]
.ModalBase
.modalBack
background-color alpha(black, 60%)
.CodeMirror
font-family inherit !important
line-height 1.4em
height 100%
.CodeMirror > div > textarea
margin-bottom -1em
.CodeMirror-focused .CodeMirror-selected
background #B1D7FE
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection
background #B1D7FE
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection
background #B1D7FE
::selection
background #B1D7FE

View File

@@ -4,27 +4,15 @@ import store from './store'
import React from 'react'
import ReactDOM from 'react-dom'
require('!!style!css!stylus?sourceMap!./global.styl')
import activityRecord from 'browser/lib/activityRecord'
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
require('./lib/ipcClient')
require('../lib/customMeta')
const electron = require('electron')
const ipc = electron.ipcRenderer
const path = require('path')
const remote = electron.remote
if (process.env.NODE_ENV !== 'production') {
window.addEventListener('keydown', function (e) {
if (e.keyCode === 73 && e.metaKey && e.altKey) {
remote.getCurrentWindow().toggleDevTools()
}
})
}
activityRecord.init()
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
})
const { remote, ipcRenderer } = electron
const { dialog } = remote
document.addEventListener('drop', function (e) {
e.preventDefault()
@@ -35,31 +23,26 @@ document.addEventListener('dragover', function (e) {
e.stopPropagation()
})
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
}
console.log(options)
return new window.Notification(title, options)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
ipc.on('copy-finder', function () {
activityRecord.emit('FINDER_COPY')
})
ipc.on('open-finder', function () {
activityRecord.emit('FINDER_OPEN')
})
let el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
function notify (...args) {
return new window.Notification(...args)
}
function updateApp () {
let index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: 'Update Boostnote',
detail: 'New Boostnote is ready to be installed.',
buttons: ['Restart & Install', 'Not Now']
})
if (index === 0) {
ipcRenderer.send('update-app-confirm')
}
}
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
@@ -80,4 +63,27 @@ ReactDOM.render((
), el, function () {
let loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
ipcRenderer.on('update-ready', function () {
store.dispatch({
type: 'UPDATE_AVAILABLE'
})
notify('Update ready!', {
body: 'New Boostnote is ready to be installed.'
})
updateApp()
})
ipcRenderer.on('update-found', function () {
notify('Update found!', {
body: 'Preparing to update...'
})
})
ipcRenderer.send('update-check', 'check-update')
window.addEventListener('online', function () {
if (!store.getState().status.updateReady) {
ipcRenderer.send('update-check', 'check-update')
}
})
})

View File

@@ -3,31 +3,39 @@ import _ from 'lodash'
const OSX = global.process.platform === 'darwin'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
const defaultConfig = {
let isInitialized = false
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
listWidth: 250,
listWidth: 280,
navWidth: 200,
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
hotkey: {
toggleFinder: OSX ? 'Cmd + Alt + S' : 'Super + Alt + S',
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E'
},
ui: {
theme: 'default',
disableDirectWrite: false
disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
},
editor: {
theme: 'xcode',
theme: 'default',
keyMap: 'sublime',
fontSize: '14',
fontFamily: 'Monaco, Consolas',
indentType: 'space',
indentSize: '4',
indentSize: '2',
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
},
preview: {
fontSize: '14',
fontFamily: 'Lato',
codeBlockTheme: 'xcode',
codeBlockTheme: 'elegant',
lineNumber: true
}
}
@@ -50,32 +58,71 @@ function get () {
let config = window.localStorage.getItem('config')
try {
config = Object.assign({}, defaultConfig, JSON.parse(config))
config = Object.assign({}, DEFAULT_CONFIG, JSON.parse(config))
config.hotkey = Object.assign({}, DEFAULT_CONFIG.hotkey, config.hotkey)
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, config.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, config.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, config.preview)
if (!validate(config)) throw new Error('INVALID CONFIG')
} catch (err) {
console.warn('Boostnote resets the malformed configuration.')
config = defaultConfig
config = DEFAULT_CONFIG
_save(config)
}
if (!isInitialized) {
isInitialized = true
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
editorTheme.setAttribute('id', 'editorTheme')
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
if (config.editor.theme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
}
return config
}
function set (updates) {
let currentConfig = get()
let newConfig = Object.assign({}, defaultConfig, currentConfig, updates)
let newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: false
})
if (newConfig.ui.theme === 'dark') {
document.body.setAttribute('data-theme', 'dark')
} else {
document.body.setAttribute('data-theme', 'default')
}
ipcRenderer.send('CONFIG_RENEW', {
config: get(),
silent: true
let editorTheme = document.getElementById('editorTheme')
if (editorTheme == null) {
editorTheme = document.createElement('link')
editorTheme.setAttribute('id', 'editorTheme')
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
let newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
? newConfig.editor.theme
: 'default'
if (newTheme !== 'default') {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
}
ipcRenderer.send('config-renew', {
config: get()
})
}
export default {
get,

View File

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

View File

@@ -0,0 +1,85 @@
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const consts = require('browser/lib/consts')
const path = require('path')
const CSON = require('@rokt33r/season')
/**
* @param {Object}
* name, path, type
*
* 1. check if BoostnoteJSON can be created
* if the file doesn't exist or isn't valid, try to rewrite it.
* if the rewriting failed, throw Error
* 2. save metadata to localStorage
* 3. fetch notes & folders
* 4. return `{storage: {...} folders: [folder]}`
*/
function addStorage (input) {
if (!_.isString(input.path)) {
return Promise.reject(new Error('Path must be a string.'))
}
let rawStorages
try {
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
} catch (e) {
console.warn(e)
rawStorages = []
}
let key = keygen()
while (rawStorages.some((storage) => storage.key === key)) {
key = keygen()
}
let newStorage = {
key,
name: input.name,
type: input.type,
path: input.path
}
return Promise.resolve(newStorage)
.then(resolveStorageData)
.then(function saveMetadataToLocalStorage (resolvedStorage) {
newStorage = resolvedStorage
rawStorages.push({
key: newStorage.key,
type: newStorage.type,
name: newStorage.name,
path: newStorage.path
})
localStorage.setItem('storages', JSON.stringify(rawStorages))
return newStorage
})
.then(function (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (!storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
}
return notes
})
})
.then(function returnValue (notes) {
return {
storage: newStorage,
notes
}
})
}
module.exports = addStorage

View File

@@ -0,0 +1,63 @@
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('@rokt33r/season')
/**
* @param {String} storageKey
* @param {Object} input
* ```
* {
* color: String,
* name: String
* }
* ```
*
* @return {Object}
* ```
* {
* storage: Object
* }
* ```
*/
function createFolder (storageKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
if (!_.isString(input.name)) throw new Error('Name must be a string.')
if (!_.isString(input.color)) throw new Error('Color must be a string.')
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function createFolder (storage) {
let key = keygen()
while (storage.folders.some((folder) => folder.key === key)) {
key = keygen()
}
let newFolder = {
key,
color: input.color,
name: input.name
}
storage.folders.push(newFolder)
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage
}
})
}
module.exports = createFolder

View File

@@ -0,0 +1,84 @@
const sander = require('sander')
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const path = require('path')
const CSON = require('@rokt33r/season')
function validateInput (input) {
if (!_.isArray(input.tags)) input.tags = []
input.tags = input.tags.filter((tag) => _.isString(tag) && tag.trim().length > 0)
if (!_.isString(input.title)) input.title = ''
input.isStarred = !!input.isStarred
switch (input.type) {
case 'MARKDOWN_NOTE':
if (!_.isString(input.content)) input.content = ''
break
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
if (!_.isArray(input.snippets)) {
input.snippets = [{
name: '',
mode: 'text',
content: ''
}]
}
break
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
}
}
function createNote (storageKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
input = Object.assign({}, input)
validateInput(input)
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function checkFolderExists (storage) {
if (_.find(storage.folders, {key: input.folder}) == null) {
throw new Error('Target folder doesn\'t exist.')
}
return storage
})
.then(function saveNote (storage) {
let key = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', key + '.cson'))
key = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
let noteData = Object.assign({}, input, {
key,
createdAt: new Date(),
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
}
module.exports = createNote

View File

@@ -0,0 +1,75 @@
const _ = require('lodash')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season')
const sander = require('sander')
/**
* @param {String} storageKey
* @param {String} folderKey
*
* @return {Object}
* ```
* {
* storage: Object,
* folderKey: String
* }
* ```
*/
function deleteFolder (storageKey, folderKey) {
let rawStorages
let targetStorage
try {
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function assignNotes (storage) {
return resolveStorageNotes(storage)
.then((notes) => {
return {
storage,
notes
}
})
})
.then(function deleteFolderAndNotes (data) {
let { storage, notes } = data
storage.folders = storage.folders
.filter(function excludeTargetFolder (folder) {
return folder.key !== folderKey
})
let targetNotes = notes.filter(function filterTargetNotes (note) {
return note.folder === folderKey
})
let deleteAllNotes = targetNotes
.map(function deleteNote (note) {
const notePath = path.join(storage.path, 'notes', note.key + '.cson')
return sander.unlink(notePath)
.catch(function (err) {
console.warn('Failed to delete', notePath, err)
})
})
return Promise.all(deleteAllNotes)
.then(() => storage)
})
.then(function (storage) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage,
folderKey
}
})
}
module.exports = deleteFolder

View File

@@ -0,0 +1,34 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const sander = require('sander')
function deleteNote (storageKey, noteKey) {
let targetStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function deleteNoteFile (storage) {
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
sander.unlinkSync(notePath)
} catch (err) {
console.warn('Failed to delete note cson', err)
}
return {
noteKey,
storageKey
}
})
}
module.exports = deleteNote

View File

@@ -0,0 +1,20 @@
const dataApi = {
init: require('./init'),
addStorage: require('./addStorage'),
renameStorage: require('./renameStorage'),
removeStorage: require('./removeStorage'),
createFolder: require('./createFolder'),
updateFolder: require('./updateFolder'),
deleteFolder: require('./deleteFolder'),
createNote: require('./createNote'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),
migrateFromV5Storage: require('./migrateFromV5Storage'),
_migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'),
_resolveStorageNotes: require('./resolveStorageNotes')
}
module.exports = dataApi

View File

@@ -0,0 +1,83 @@
'use strict'
const _ = require('lodash')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const consts = require('browser/lib/consts')
const path = require('path')
const CSON = require('@rokt33r/season')
/**
* @return {Object} all storages and notes
* ```
* {
* storages: [...],
* notes: [...]
* }
* ```
*
* This method deals with 3 patterns.
* 1. v1
* 2. legacy
* 3. empty directory
*/
function init () {
let fetchStorages = function () {
let rawStorages
try {
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
} catch (e) {
console.warn('Failed to parse cached data from localStorage', e)
rawStorages = []
window.localStorage.setItem('storages', JSON.stringify(rawStorages))
}
return Promise.all(rawStorages
.map(resolveStorageData))
}
let fetchNotes = function (storages) {
let findNotesFromEachStorage = storages
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {
let unknownCount = 0
notes.forEach((note) => {
if (!storage.folders.some((folder) => note.folder === folder.key)) {
unknownCount++
storage.folders.push({
key: note.folder,
color: consts.FOLDER_COLORS[(unknownCount - 1) % 7],
name: 'Unknown ' + unknownCount
})
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
}
return notes
})
})
return Promise.all(findNotesFromEachStorage)
.then(function concatNoteGroup (noteGroups) {
return noteGroups.reduce(function (sum, group) {
return sum.concat(group)
}, [])
})
.then(function returnData (notes) {
return {
storages,
notes
}
})
}
return Promise.resolve(fetchStorages())
.then((storages) => {
return storages
.filter((storage) => {
if (!_.isObject(storage)) return false
return true
})
})
.then(fetchNotes)
}
module.exports = init

View File

@@ -0,0 +1,110 @@
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const resolveStorageData = require('./resolveStorageData')
const consts = require('browser/lib/consts')
const CSON = require('@rokt33r/season')
const path = require('path')
const sander = require('sander')
function migrateFromV5Storage (storageKey, data) {
let targetStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function (storage) {
return importAll(storage, data)
})
}
function importAll (storage, data) {
let oldArticles = data.articles
let notes = []
data.folders
.forEach(function (oldFolder) {
let folderKey = keygen()
while (storage.folders.some((folder) => folder.key === folderKey)) {
folderKey = keygen()
}
let newFolder = {
key: folderKey,
name: oldFolder.name,
color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7]
}
storage.folders.push(newFolder)
let articles = oldArticles.filter((article) => article.FolderKey === oldFolder.key)
articles.forEach((article) => {
let noteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(storage.path, 'notes', noteKey + '.cson'))
noteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
console.error('Failed to read `notes` directory.')
throw err
}
}
}
if (article.mode === 'markdown') {
let newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'MARKDOWN_NOTE',
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
}
notes.push(newNote)
} else {
let newNote = {
tags: article.tags,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
folder: folderKey,
storage: storage.key,
type: 'SNIPPET_NOTE',
isStarred: false,
title: article.title,
description: article.title,
key: noteKey,
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
}]
}
notes.push(newNote)
}
})
})
notes.forEach(function (note) {
CSON.writeFileSync(path.join(storage.path, 'notes', note.key + '.cson'), _.omit(note, ['storage', 'key']))
})
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['version', 'folders']))
return {
storage,
notes
}
}
module.exports = migrateFromV5Storage

View File

@@ -0,0 +1,89 @@
const path = require('path')
const sander = require('sander')
const keygen = require('browser/lib/keygen')
const _ = require('lodash')
const CSON = require('@rokt33r/season')
function migrateFromV5Storage (storagePath) {
var boostnoteJSONPath = path.join(storagePath, 'boostnote.json')
return Promise.resolve()
.then(function readBoostnoteJSON () {
return sander.readFile(boostnoteJSONPath, {
encoding: 'utf-8'
})
})
.then(function verifyVersion (rawData) {
var boostnoteJSONData = JSON.parse(rawData)
if (boostnoteJSONData.version === '1.0') throw new Error('Target storage seems to be transformed already.')
if (!_.isArray(boostnoteJSONData.folders)) throw new Error('the value of folders is not an array.')
return boostnoteJSONData
})
.then(function setVersion (boostnoteJSONData) {
boostnoteJSONData.version = '1.0'
return sander.writeFile(boostnoteJSONPath, JSON.stringify(boostnoteJSONData))
.then(() => boostnoteJSONData)
})
.then(function fetchNotes (boostnoteJSONData) {
var fetchNotesFromEachFolder = boostnoteJSONData.folders
.map(function (folder) {
const folderDataJSONPath = path.join(storagePath, folder.key, 'data.json')
return sander
.readFile(folderDataJSONPath, {
encoding: 'utf-8'
})
.then(function (rawData) {
var data = JSON.parse(rawData)
if (!_.isArray(data.notes)) throw new Error('value of notes is not an array.')
return data.notes
.map(function setFolderToNote (note) {
note.folder = folder.key
return note
})
})
.catch(function failedToReadDataJSON (err) {
console.warn('Failed to fetch notes from ', folderDataJSONPath, err)
return []
})
})
return Promise.all(fetchNotesFromEachFolder)
.then(function flatten (folderNotes) {
return folderNotes
.reduce(function concatNotes (sum, notes) {
return sum.concat(notes)
}, [])
})
.then(function saveNotes (notes) {
notes.forEach(function renewKey (note) {
var newKey = keygen()
while (notes.some((_note) => _note.key === newKey)) {
newKey = keygen()
}
note.key = newKey
})
const noteDirPath = path.join(storagePath, 'notes')
notes
.map(function saveNote (note) {
CSON.writeFileSync(path.join(noteDirPath, note.key) + '.cson', note)
})
return true
})
.then(function deleteFolderDir (check) {
if (check) {
boostnoteJSONData.folders.forEach((folder) => {
sander.rimrafSync(path.join(storagePath, folder.key))
})
}
return check
})
})
.catch(function handleError (err) {
console.warn(err)
return false
})
}
module.exports = migrateFromV5Storage

View File

@@ -0,0 +1,90 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage
try {
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Storage doesn\'t exist.')
oldStorage = _.find(cachedStorageList, {key: storageKey})
if (oldStorage == null) throw new Error('Storage doesn\'t exist.')
newStorage = _.find(cachedStorageList, {key: newStorageKey})
if (newStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(oldStorage)
.then(function saveNote (_oldStorage) {
oldStorage = _oldStorage
let noteData
let notePath = path.join(oldStorage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
throw err
}
let newNoteKey
return Promise.resolve()
.then(function resolveNewStorage () {
if (storageKey === newStorageKey) {
newNoteKey = noteKey
return oldStorage
}
return resolveStorageData(newStorage)
.then(function findNewNoteKey (_newStorage) {
newStorage = _newStorage
newNoteKey = keygen()
let isUnique = false
while (!isUnique) {
try {
sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson'))
newNoteKey = keygen()
} catch (err) {
if (err.code === 'ENOENT') {
isUnique = true
} else {
throw err
}
}
}
return newStorage
})
})
.then(function checkFolderExistsAndPrepareNoteData (newStorage) {
if (_.find(newStorage.folders, {key: newFolderKey}) == null) throw new Error('Target folder doesn\'t exist.')
noteData.folder = newFolderKey
noteData.key = newNoteKey
noteData.storage = newStorageKey
noteData.updatedAt = new Date()
return noteData
})
.then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
.then(function deleteOldNote (data) {
if (storageKey !== newStorageKey) {
try {
sander.unlinkSync(path.join(oldStorage.path, 'notes', noteKey + '.cson'))
} catch (err) {
console.warn(err)
}
}
return data
})
})
}
module.exports = moveNote

View File

@@ -0,0 +1,30 @@
const _ = require('lodash')
/**
* @param {String} key
* @return {key}
*/
function removeStorage (key) {
let rawStorages
try {
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('invalid storages')
} catch (e) {
console.warn(e)
rawStorages = []
}
rawStorages = rawStorages
.filter(function excludeTargetStorage (rawStorage) {
return rawStorage.key !== key
})
localStorage.setItem('storages', JSON.stringify(rawStorages))
return Promise.resolve({
storageKey: key
})
}
module.exports = removeStorage

View File

@@ -0,0 +1,31 @@
const _ = require('lodash')
const resolveStorageData = require('./resolveStorageData')
/**
* @param {String} key
* @param {String} name
* @return {Object} Storage meta data
*/
function renameStorage (key, name) {
if (!_.isString(name)) return Promise.reject(new Error('Name must be a string.'))
let cachedStorageList
try {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
console.log('error got')
console.error(err)
return Promise.reject(err)
}
let targetStorage = _.find(cachedStorageList, {key: key})
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.name = name
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
targetStorage.path
return resolveStorageData(targetStorage)
}
module.exports = renameStorage

View File

@@ -0,0 +1,44 @@
const _ = require('lodash')
const path = require('path')
const CSON = require('@rokt33r/season')
const migrateFromV6Storage = require('./migrateFromV6Storage')
function resolveStorageData (storageCache) {
let storage = {
key: storageCache.key,
name: storageCache.name,
type: storageCache.type,
path: storageCache.path
}
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
try {
let jsonData = CSON.readFileSync(boostnoteJSONPath)
if (!_.isArray(jsonData.folders)) throw new Error('folders should be an array.')
storage.folders = jsonData.folders
storage.version = jsonData.version
} catch (err) {
if (err.code === 'ENOENT') {
console.warn('boostnote.json file doesn\'t exist the given path')
CSON.writeFileSync(boostnoteJSONPath, {folders: [], version: '1.0'})
} else {
console.error(err)
}
storage.folders = []
storage.version = '1.0'
}
let version = parseInt(storage.version, 10)
if (version >= 1) {
if (version > 1) {
console.log('The repository version is newer than one of current app.')
}
return Promise.resolve(storage)
}
console.log('Transform Legacy storage', storage.path)
return migrateFromV6Storage(storage.path)
.then(() => storage)
}
module.exports = resolveStorageData

View File

@@ -0,0 +1,38 @@
const sander = require('sander')
const path = require('path')
const CSON = require('@rokt33r/season')
function resolveStorageNotes (storage) {
const notesDirPath = path.join(storage.path, 'notes')
let notePathList
try {
notePathList = sander.readdirSync(notesDirPath)
} catch (err) {
if (err.code === 'ENOENT') {
console.log(notesDirPath, ' doesn\'t exist.')
sander.mkdirSync(notesDirPath)
} else {
console.warn('Failed to find note dir', notesDirPath, err)
}
notePathList = []
}
let notes = notePathList
.filter(function filterOnlyCSONFile (notePath) {
return /\.cson$/.test(notePath)
})
.map(function parseCSONFile (notePath) {
try {
let data = CSON.readFileSync(path.join(notesDirPath, notePath))
data.key = path.basename(notePath, '.cson')
data.storage = storage.key
return data
} catch (err) {
console.error(notePath)
throw err
}
})
return Promise.resolve(notes)
}
module.exports = resolveStorageNotes

View File

@@ -0,0 +1,56 @@
const _ = require('lodash')
const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const CSON = require('@rokt33r/season')
/**
* @param {String} storageKey
* @param {String} folderKey
* @param {Object} input
* ```
* {
* color: String,
* name: String
* }
* ```
*
* @return {Object}
* ```
* {
* storage: Object
* }
* ```
*/
function updateFolder (storageKey, folderKey, input) {
let rawStorages
let targetStorage
try {
if (input == null) throw new Error('No input found.')
if (!_.isString(input.name)) throw new Error('Name must be a string.')
if (!_.isString(input.color)) throw new Error('Color must be a string.')
rawStorages = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(rawStorages)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(rawStorages, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function updateFolder (storage) {
let targetFolder = _.find(storage.folders, {key: folderKey})
if (targetFolder == null) throw new Error('Target folder doesn\'t exist.')
targetFolder.name = input.name
targetFolder.color = input.color
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
return {
storage
}
})
}
module.exports = updateFolder

View File

@@ -0,0 +1,123 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const CSON = require('@rokt33r/season')
function validateInput (input) {
let validatedInput = {}
if (input.tags != null) {
if (!_.isArray(input.tags)) validatedInput.tags = []
validatedInput.tags = input.tags
.filter((tag) => _.isString(tag) && tag.trim().length > 0)
}
if (input.title != null) {
if (!_.isString(input.title)) validatedInput.title = ''
else validatedInput.title = input.title
}
if (input.isStarred != null) {
validatedInput.isStarred = !!input.isStarred
}
validatedInput.type = input.type
switch (input.type) {
case 'MARKDOWN_NOTE':
if (input.content != null) {
if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content
}
return validatedInput
case 'SNIPPET_NOTE':
if (input.description != null) {
if (!_.isString(input.description)) validatedInput.description = ''
else validatedInput.description = input.description
}
if (input.snippets != null) {
if (!_.isArray(input.snippets)) {
validatedInput.snippets = [{
name: '',
mode: 'text',
content: ''
}]
} else {
validatedInput.snippets = input.snippets
}
validatedInput.snippets
.filter((snippet) => {
if (!_.isString(snippet.name)) return false
if (!_.isString(snippet.mode)) return false
if (!_.isString(snippet.content)) return false
return true
})
}
return validatedInput
default:
throw new Error('Invalid type: only MARKDOWN_NOTE and SNIPPET_NOTE are available.')
}
}
function updateNote (storageKey, noteKey, input) {
let targetStorage
try {
if (input == null) throw new Error('No input found.')
input = validateInput(input)
let cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.')
targetStorage = _.find(cachedStorageList, {key: storageKey})
if (targetStorage == null) throw new Error('Target storage doesn\'t exist.')
} catch (e) {
return Promise.reject(e)
}
return resolveStorageData(targetStorage)
.then(function saveNote (storage) {
let noteData
let notePath = path.join(storage.path, 'notes', noteKey + '.cson')
try {
noteData = CSON.readFileSync(notePath)
} catch (err) {
console.warn('Failed to find note cson', err)
noteData = input.type === 'SNIPPET_NOTE'
? {
type: 'SNIPPET_NOTE',
description: [],
snippets: [{
name: '',
mode: 'text',
content: ''
}]
}
: {
type: 'MARKDOWN_NOTE',
content: ''
}
noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')
noteData.folder = storage.folders[0].key
noteData.createdAt = new Date()
noteData.updatedAt = new Date()
noteData.isStarred = false
noteData.tags = []
}
if (noteData.type === 'SNIPPET_NOTE') {
noteData.title
}
Object.assign(noteData, input, {
key: noteKey,
updatedAt: new Date(),
storage: storageKey
})
CSON.writeFileSync(path.join(storage.path, 'notes', noteKey + '.cson'), _.omit(noteData, ['key', 'storage']))
return noteData
})
}
module.exports = updateNote

View File

@@ -0,0 +1,44 @@
import ConfigManager from './ConfigManager'
import store from 'browser/main/store'
const nodeIpc = require('node-ipc')
const { remote, ipcRenderer } = require('electron')
const { app } = remote
const path = require('path')
nodeIpc.config.id = 'main'
nodeIpc.config.retry = 1500
nodeIpc.config.silent = true
nodeIpc.connectTo(
'node',
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
console.log(err)
})
nodeIpc.of.node.on('connect', function () {
console.log('Conncted successfully')
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
})
nodeIpc.of.node.on('disconnect', function () {
console.log('disconnected')
})
nodeIpc.of.node.on('request-data-from-finder', function () {
console.log('throttle')
var { data } = store.getState()
console.log(data.starredSet.toJS())
nodeIpc.of.node.emit('throttle-data', {
storageMap: data.storageMap.toJS(),
noteMap: data.noteMap.toJS(),
starredSet: data.starredSet.toJS(),
storageNoteMap: data.storageNoteMap.toJS(),
folderNoteMap: data.folderNoteMap.toJS(),
tagNoteMap: data.tagNoteMap.toJS()
})
})
}
)
module.exports = nodeIpc

View File

@@ -0,0 +1,121 @@
import store from 'browser/main/store'
const _ = require('lodash')
const keygen = require('browser/lib/keygen')
const Mixpanel = require('mixpanel')
const mixpanel = Mixpanel.init('7a0aca437d72dfd07cbcbf58d3b61f27', {key: 'fde4fd23f4d550f1b646bcd7d4374b1f'})
const moment = require('moment')
const electron = require('electron')
function _getClientKey () {
let clientKey = localStorage.getItem('clientKey')
if (!_.isString(clientKey) || clientKey.length !== 40) {
clientKey = keygen(20)
_setClientKey(clientKey)
}
return clientKey
}
function _setClientKey (newKey) {
localStorage.setItem('clientKey', newKey)
}
function _fetch () {
let events
try {
events = JSON.parse(localStorage.getItem('events'))
if (!_.isArray(events)) throw new Error('events is not an array.')
} catch (err) {
console.warn(err)
events = []
localStorage.setItem('events', JSON.stringify(events))
console.info('Events cache initialzed')
}
return events
}
function _keep (name, properties) {
let events = _fetch()
properties.time = new Date()
events.push({
name,
properties
})
localStorage.setItem('events', JSON.stringify(events))
}
function _keepUnique (name, properties) {
let events = _fetch()
properties.time = new Date()
events = events.filter((event) => event.name !== name)
events.push({
name,
properties
})
localStorage.setItem('events', JSON.stringify(events))
}
function _flush () {
let events = _fetch()
let spliced = events.splice(0, 50)
localStorage.setItem('events', JSON.stringify(events))
if (spliced.length > 0) {
let parsedEvents = spliced
.filter((event) => {
if (!_.isObject(event)) return false
if (!_.isString(event.name)) return false
if (!_.isObject(event.properties)) return false
if (!moment(event.properties.time).isValid()) return false
if (new Date() - moment(event.properties.time).toDate() > 1000 * 3600 * 24 * 3) return false
return true
})
.map((event) => {
return {
event: event.name,
properties: event.properties
}
})
mixpanel.import_batch(parsedEvents, {}, (errs) => {
if (errs.length > 0) {
let events = _fetch()
events = events.concat(spliced)
localStorage.setItem('events', JSON.stringify(events))
} else {
_flush()
}
})
let state = store.getState()
mixpanel.people.set(_getClientKey(), {
storage_count: state.data.storageMap.size,
note_count: state.data.noteMap.size,
version: electron.remote.app.getVersion()
})
}
}
setInterval(_flush, 1000 * 60 * 60)
function track (name, properties) {
switch (name) {
case 'MAIN_FOCUSED':
properties = Object.assign({}, properties, {
distinct_id: _getClientKey()
})
_keepUnique(name, properties)
break
default:
properties = Object.assign({}, properties, {
distinct_id: _getClientKey()
})
_keep(name, properties)
}
}
module.exports = {
_mp: mixpanel,
track
}

View File

@@ -0,0 +1,11 @@
const path = require('path')
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)
}
export default notify

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