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

Compare commits

...

419 Commits

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

Conflicts:
	package.json
2016-01-22 03:01:13 +09:00
Rokt33r
e921e30d64 bump up version to v0.5.4 2016-01-22 02:59:35 +09:00
Rokt33r
cd4f9d8bb4 emit ARTICLE_CREATE when new post clicked 2016-01-22 02:58:06 +09:00
Rokt33r
a0553788b6 update h1 style of markdown 2016-01-22 02:55:02 +09:00
Rokt33r
1a183d78af fix go to line end 2016-01-21 08:29:09 +09:00
Rokt33r
cabcaa892c clean old code 2016-01-21 05:50:53 +09:00
Rokt33r
01c9d62a2b fix sanitizing bug in Code block 2016-01-21 05:50:40 +09:00
Rokt33r
ba76df863c bump up version 2016-01-19 20:13:17 +09:00
Rokt33r
81441a0895 done but bugged 2016-01-19 20:11:38 +09:00
Rokt33r
da0222f213 esc to show preview when rightclick to toggle set 2016-01-19 18:58:27 +09:00
Rokt33r
fb8a2eb2e0 show proper error when hotkey updated 2016-01-19 18:47:35 +09:00
Rokt33r
cde2e27e04 must override default config 2016-01-19 18:47:10 +09:00
203 changed files with 14719 additions and 8885 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" } ]
]
}
}
}

4
.gitignore vendored
View File

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

4
.gitmodules vendored
View File

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

20
LICENSE
View File

@@ -1,2 +1,18 @@
本製品をインストール、または使用することによって、お客様は利用規約(
https://b00st.io/regulations.htmlより拘束されることに承諾されたものとします。利用規約に同意されない場合、Boostnoteは、お客様に本製品のインストール、使用のいずれも許諾できません。
GPL-3.0
Boostnote - the simplest note app
Copyright (C) 2016 MAISIN&CO.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -1,183 +1,173 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import modes from '../lib/modes'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
import CodeMirror from 'codemirror'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const ace = window.ace
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
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.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.changeHandler = e => this.handleChange(e)
this.blurHandler = (e) => {
if (e.relatedTarget === null) {
return
this.changeHandler = (e) => this.handleChange(e)
this.blurHandler = (editor, e) => {
if (e == null) return null
let el = e.relatedTarget
while (el != null) {
if (el === this.refs.root) {
return
}
el = el.parentNode
}
let isFocusingToSearch = e.relatedTarget.className && e.relatedTarget.className.split(' ').some(clss => {
return clss === 'ace_search_field' || clss === 'ace_searchbtn' || clss === 'ace_replacebtn' || clss === 'ace_searchbtn_close' || clss === 'ace_text-input'
})
if (isFocusingToSearch) {
return
}
if (this.props.onBlur) this.props.onBlur(e)
}
this.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)
})
}
}
this.state = {
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size']
}
this.silentChange = false
}
componentWillReceiveProps (nextProps) {
if (nextProps.readOnly !== this.props.readOnly) {
this.editor.setReadOnly(!!nextProps.readOnly)
this.props.onBlur != null && this.props.onBlur(e)
}
}
componentDidMount () {
let { article } = this.props
var el = ReactDOM.findDOMNode(this)
var editor = this.editor = ace.edit(el)
editor.$blockScrolling = Infinity
editor.renderer.setShowGutter(true)
editor.setTheme('ace/theme/xcode')
editor.moveCursorTo(0, 0)
editor.setReadOnly(!!this.props.readOnly)
editor.setFontSize(this.state.fontSize)
editor.on('blur', this.blurHandler)
editor.commands.addCommand({
name: 'Emacs cursor up',
bindKey: {mac: 'Ctrl-P'},
exec: function (editor) {
editor.navigateUp(1)
if (editor.getCursorPosition().row < editor.getFirstVisibleRow()) editor.scrollToLine(editor.getCursorPosition().row, false, false)
},
readOnly: true
})
editor.commands.addCommand({
name: 'Focus title',
bindKey: {win: 'Esc', mac: 'Esc'},
exec: function (editor, e) {
remote.getCurrentWebContents().send('list-focus')
},
readOnly: true
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: 'sublime',
inputStyle: 'textarea',
extraKeys: {
Tab: function (cm) {
if (cm.somethingSelected()) cm.indentSelection('add')
else {
if (cm.getOption('indentWithTabs')) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
}
},
'Cmd-T': function (cm) {
// Do nothing
}
}
})
editor.commands.on('afterExec', this.afterExecHandler)
this.setMode(this.props.mode)
var session = editor.getSession()
let mode = _.findWhere(modes, {name: article.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
session.setOption('useWorker', false)
session.setUseWrapMode(true)
session.setValue(this.props.article.content)
session.on('change', this.changeHandler)
ipc.on('config-apply', this.configApplyHandler)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
}
componentWillUnmount () {
ipc.removeListener('config-apply', this.configApplyHandler)
this.editor.getSession().removeListener('change', this.changeHandler)
this.editor.removeListener('blur', this.blurHandler)
this.editor.commands.removeListener('afterExec', this.afterExecHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
}
componentDidUpdate (prevProps, prevState) {
var session = this.editor.getSession()
if (this.props.article.key !== prevProps.article.key) {
session.removeListener('change', this.changeHandler)
session.setValue(this.props.article.content)
session.getUndoManager().reset()
session.on('change', this.changeHandler)
let needRefresh = false
if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode)
}
if (prevProps.article.mode !== this.props.article.mode) {
let mode = _.findWhere(modes, {name: this.props.article.mode})
let syntaxMode = mode != null
? mode.mode
: 'text'
session.setMode('ace/mode/' + syntaxMode)
if (prevProps.theme !== this.props.theme) {
this.editor.setOption('theme', this.props.theme)
needRefresh = true
}
if (prevProps.fontSize !== this.props.fontSize) {
needRefresh = true
}
if (prevProps.fontFamily !== this.props.fontFamily) {
needRefresh = true
}
if (prevProps.indentSize !== this.props.indentSize) {
this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize)
}
if (prevProps.indentType !== this.props.indentType) {
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
}
if (needRefresh) {
this.editor.refresh()
}
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['editor-font-size'],
fontFamily: config['editor-font-family'],
indentType: config['editor-indent-type'],
indentSize: config['editor-indent-size']
}, function () {
var session = this.editor.getSession()
session.setUseSoftTabs(this.state.indentType === 'space')
session.setTabSize(!isNaN(this.state.indentSize) ? parseInt(this.state.indentSize, 10) : 4)
})
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) {
this.value = this.editor.getValue()
if (this.props.onChange) {
var value = this.editor.getValue()
this.props.onChange(value)
this.props.onChange(e)
}
}
getFirstVisibleRow () {
return this.editor.getFirstVisibleRow()
}
getCursorPosition () {
return this.editor.getCursorPosition()
}
moveCursorTo (row, col) {
this.editor.moveCursorTo(row, col)
}
scrollToLine (num) {
this.editor.scrollToLine(num, false, false)
}
focus () {
this.editor.focus()
}
blur () {
this.editor.blur()
}
reload () {
// 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)
}
setValue (value) {
let cursor = this.editor.getCursor()
this.editor.setValue(value)
this.editor.setCursor(cursor)
}
render () {
let { className, fontFamily, fontSize } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return (
<div
className={this.props.className == null ? 'CodeEditor' : 'CodeEditor ' + this.props.className}
className={className == null
? 'CodeEditor'
: `CodeEditor ${className}`
}
ref='root'
tabIndex='-1'
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + ', monospace'
fontFamily: fontFamily.join(', '),
fontSize: fontSize
}}
/>
)
@@ -185,11 +175,8 @@ export default class CodeEditor extends React.Component {
}
CodeEditor.propTypes = {
article: PropTypes.shape({
content: PropTypes.string,
mode: PropTypes.string,
key: PropTypes.string
}),
value: PropTypes.string,
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,
onChange: PropTypes.func,
@@ -197,7 +184,10 @@ CodeEditor.propTypes = {
}
CodeEditor.defaultProps = {
readOnly: false
readOnly: false,
theme: 'xcode',
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
}
export default CodeEditor

View File

@@ -1,20 +0,0 @@
import React, { PropTypes } from 'react'
const electron = require('electron')
const shell = electron.shell
export default class ExternalLink extends React.Component {
handleClick (e) {
shell.openExternal(this.props.href)
e.preventDefault()
}
render () {
return (
<a onClick={e => this.handleClick(e)} {...this.props}/>
)
}
}
ExternalLink.propTypes = {
href: PropTypes.string
}

View File

@@ -1,52 +0,0 @@
import React, { PropTypes } from 'react'
const BLUE = '#3460C7'
const LIGHTBLUE = '#2BA5F7'
const ORANGE = '#FF8E00'
const YELLOW = '#E8D252'
const GREEN = '#3FD941'
const DARKGREEN = '#1FAD85'
const RED = '#E10051'
const PURPLE = '#B013A4'
function getColorByIndex (index) {
switch (index % 8) {
case 0:
return RED
case 1:
return ORANGE
case 2:
return YELLOW
case 3:
return GREEN
case 4:
return DARKGREEN
case 5:
return LIGHTBLUE
case 6:
return BLUE
case 7:
return PURPLE
default:
return DARKGREEN
}
}
export default class FolderMark extends React.Component {
render () {
let color = getColorByIndex(this.props.color)
let className = 'FolderMark fa fa-square fa-fw'
if (this.props.className != null) {
className += ' active'
}
return (
<i className={className} style={{color: color}}/>
)
}
}
FolderMark.propTypes = {
color: PropTypes.number,
className: PropTypes.string
}

View File

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

View File

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

View File

@@ -1,211 +1,187 @@
import React, { PropTypes } from 'react'
import markdown from '../lib/markdown'
import ReactDOM from 'react-dom'
import sanitizeHtml from '@rokt33r/sanitize-html'
import markdown from 'browser/lib/markdown'
import _ from 'lodash'
import fetchConfig from '../lib/fetchConfig'
import hljsTheme from 'browser/lib/hljsThemes'
const electron = require('electron')
const shell = electron.shell
const ipc = electron.ipcRenderer
const katex = window.katex
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
const OSX = global.process.platform === 'darwin'
const sanitizeOpts = {
allowedTags: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'img', 'span', 'cite', 'del', 'u', 'sub', 'sup', 's', 'input', 'label' ],
allowedClasses: {
'a': ['lineAnchor'],
'div': ['math'],
'span': ['math', 'hljs-*'],
'code': ['language-*']
},
allowedAttributes: {
a: ['href', 'data-key'],
img: [ 'src' ],
label: ['for'],
input: ['checked', 'type'],
'*': ['id', 'name']
},
transformTags: {
'*': function (tagName, attribs) {
let href = attribs.href
if (tagName === 'input' && attribs.type !== 'checkbox') {
return false
}
if (_.isString(href) && href.match(/^#.+$/)) attribs.href = href.replace(/^#/, '#md-anchor-')
if (attribs.id) attribs.id = 'md-anchor-' + attribs.id
if (attribs.name) attribs.name = 'md-anchor-' + attribs.name
if (attribs.for) attribs.for = 'md-anchor-' + attribs.for
return {
tagName: tagName,
attribs: attribs
}
}
}
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
if (!OSX) {
defaultFontFamily.unshift('\'Microsoft YaHei\'')
defaultFontFamily.unshift('meiryo')
}
function handleAnchorClick (e) {
if (e.target.attributes.href && e.target.attributes.href.nodeValue.match(/#.+/)) {
return
}
e.preventDefault()
e.stopPropagation()
let href = e.target.href
if (href.match(/^http:\/\/|https:\/\/|mailto:\/\//)) {
shell.openExternal(href)
}
}
function stopPropagation (e) {
e.preventDefault()
e.stopPropagation()
}
function math2Katex (display) {
return function (el) {
try {
katex.render(el.innerHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&'), el, {display: display})
el.className = 'math-rendered'
} catch (e) {
el.innerHTML = e.message
el.className = 'math-failed'
}
}
}
let config = fetchConfig()
ipc.on('config-apply', function (e, newConfig) {
config = newConfig
})
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
this.configApplyHandler = (e, config) => this.handleConfigApply(e, config)
this.state = {
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family']
}
}
componentDidMount () {
this.addListener()
this.renderMath()
ipc.on('config-apply', this.configApplyHandler)
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)
}
componentDidUpdate () {
this.addListener()
this.renderMath()
}
handlePreviewAnchorClick (e) {
e.preventDefault()
e.stopPropagation()
componentWillUnmount () {
this.removeListener()
ipc.removeListener('config-apply', this.configApplyHandler)
}
componentWillUpdate () {
this.removeListener()
}
renderMath () {
let inline = ReactDOM.findDOMNode(this).querySelectorAll('span.math')
Array.prototype.forEach.call(inline, math2Katex(false))
let block = ReactDOM.findDOMNode(this).querySelectorAll('div.math')
Array.prototype.forEach.call(block, math2Katex(true))
}
addListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.addEventListener('click', handleAnchorClick)
anchor.addEventListener('mousedown', stopPropagation)
anchor.addEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.addEventListener('click', stopPropagation)
})
}
removeListener () {
var anchors = ReactDOM.findDOMNode(this).querySelectorAll('a:not(.lineAnchor)')
var inputs = ReactDOM.findDOMNode(this).querySelectorAll('input')
Array.prototype.forEach.call(anchors, anchor => {
anchor.removeEventListener('click', handleAnchorClick)
anchor.removeEventListener('mousedown', stopPropagation)
anchor.removeEventListener('mouseup', stopPropagation)
})
Array.prototype.forEach.call(inputs, input => {
input.removeEventListener('click', stopPropagation)
})
}
handleClick (e) {
if (this.props.onClick) {
this.props.onClick(e)
let href = e.target.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(e.target.href)
}
}
handleDoubleClick (e) {
if (this.props.onDoubleClick) {
this.props.onDoubleClick(e)
}
handleCheckboxClick (e) {
this.props.onCheckboxClick(e)
}
handleContextMenu (e) {
this.props.onContextMenu(e)
}
handleMouseDown (e) {
if (this.props.onMouseDown) {
this.props.onMouseDown(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 (this.props.onMouseUp) {
this.props.onMouseUp(e)
if (e.target != null && e.target.tagName === 'A') {
return null
}
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
handleMouseMove (e) {
if (this.props.onMouseMove) {
this.props.onMouseMove(e)
}
componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-same-origin')
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
this.rewriteIframe()
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
}
handleConfigApply (e, config) {
this.setState({
fontSize: config['preview-font-size'],
fontFamily: config['preview-font-family']
componentWillUnmount () {
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value ||
prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber ||
prevProps.theme !== this.props.theme
) this.rewriteIframe()
}
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', this.anchorClickHandler)
})
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.removeEventListener('click', this.checkboxClickHandler)
})
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, theme } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? [fontFamily].concat(defaultFontFamily)
: defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? [codeBlockFontFamily].concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
codeBlockTheme = hljsTheme().some((theme) => theme.name === codeBlockTheme) ? codeBlockTheme : 'xcode'
this.refs.root.contentWindow.document.head.innerHTML = `
<style>
@font-face {
font-family: 'Lato';
src: url('../resources/fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */
url('../resources/fonts/Lato-Regular.ttf') format('truetype');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
${markdownStyle}
body {
font-family: ${fontFamily.join(', ')};
font-size: ${fontSize}px;
}
code {
font-family: ${codeBlockFontFamily.join(', ')};
}
.lineNumber {
${lineNumber && 'display: block !important;'}
font-family: ${codeBlockFontFamily.join(', ')};
opacity: 0.5;
}
</style>
<link rel="stylesheet" href="../node_modules/highlight.js/styles/${codeBlockTheme}.css">
<link rel="stylesheet" href="../compiled/katex-style.css">
`
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
})
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.addEventListener('click', this.checkboxClickHandler)
})
}
render () {
let isEmpty = this.props.content.trim().length === 0
let content = isEmpty
? '(Empty content)'
: this.props.content
content = markdown(content)
content = sanitizeHtml(content, sanitizeOpts)
focus () {
this.refs.root.focus()
}
getWindow () {
return this.refs.root.contentWindow
}
scrollTo (targetRow) {
let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
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
}
}
}
render () {
let { className, style, tabIndex } = this.props
return (
<div
className={'MarkdownPreview' + (this.props.className != null ? ' ' + this.props.className : '') + (isEmpty ? ' empty' : '')}
onClick={e => this.handleClick(e)}
onDoubleClick={e => this.handleDoubleClick(e)}
onMouseDown={e => this.handleMouseDown(e)}
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}
dangerouslySetInnerHTML={{__html: ' ' + content}}
style={{
fontSize: this.state.fontSize,
fontFamily: this.state.fontFamily.trim() + (OSX ? '' : ', meiryo, \'Microsoft YaHei\'') + ', helvetica, arial, sans-serif'
}}
<iframe className={className != null
? 'MarkdownPreview ' + className
: 'MarkdownPreview'
}
style={style}
tabIndex={tabIndex}
ref='root'
/>
)
}
@@ -216,7 +192,6 @@ MarkdownPreview.propTypes = {
onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onMouseMove: PropTypes.func,
className: PropTypes.string,
content: 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

@@ -1,168 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import _ from 'lodash'
import linkState from '../lib/linkState'
function isNotEmptyString (str) {
return _.isString(str) && str.length > 0
}
export default class TagSelect extends React.Component {
constructor (props) {
super(props)
this.state = {
input: '',
isInputFocused: false
}
}
componentDidMount () {
this.blurInputBlurHandler = e => {
if (ReactDOM.findDOMNode(this.refs.tagInput) !== document.activeElement) {
this.setState({isInputFocused: false})
}
}
window.addEventListener('click', this.blurInputBlurHandler)
}
componentWillUnmount (e) {
window.removeEventListener('click', this.blurInputBlurHandler)
}
// Suggestは必ずInputの下に位置するようにする
componentDidUpdate () {
if (this.shouldShowSuggest()) {
let inputRect = ReactDOM.findDOMNode(this.refs.tagInput).getBoundingClientRect()
let suggestElement = ReactDOM.findDOMNode(this.refs.suggestTags)
if (suggestElement != null) {
suggestElement.style.top = inputRect.top + 20 + 'px'
suggestElement.style.left = inputRect.left + 'px'
}
}
}
shouldShowSuggest () {
return this.state.isInputFocused && isNotEmptyString(this.state.input)
}
addTag (tag, clearInput = true) {
let tags = this.props.tags.slice(0)
let newTag = tag.trim()
if (newTag.length === 0 && clearInput) {
this.setState({input: ''})
return
}
tags.push(newTag)
tags = _.uniq(tags)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(newTag, tags)
}
if (clearInput) this.setState({input: ''})
}
handleKeyDown (e) {
switch (e.keyCode) {
case 8:
{
if (this.state.input.length > 0) break
e.preventDefault()
let tags = this.props.tags.slice(0)
tags.pop()
this.props.onChange(null, tags)
}
break
case 13:
{
e.preventDefault()
this.addTag(this.state.input)
}
}
}
handleThisClick (e) {
ReactDOM.findDOMNode(this.refs.tagInput).focus()
}
handleInputFocus (e) {
this.setState({isInputFocused: true})
}
handleItemRemoveButton (tag) {
return e => {
e.stopPropagation()
let tags = this.props.tags.slice(0)
tags.splice(tags.indexOf(tag), 1)
if (_.isFunction(this.props.onChange)) {
this.props.onChange(null, tags)
}
}
}
handleSuggestClick (tag) {
return e => {
this.addTag(tag)
}
}
render () {
let { tags, suggestTags } = this.props
let tagElements = _.isArray(tags)
? this.props.tags.map(tag => (
<div key={tag} className='TagSelect-tags-item'>
<button onClick={e => this.handleItemRemoveButton(tag)(e)} className='TagSelect-tags-item-remove'><i className='fa fa-fw fa-times'/></button>
<div className='TagSelect-tags-item-label'>{tag}</div>
</div>))
: null
let suggestElements = this.shouldShowSuggest() ? suggestTags
.filter(tag => {
return tag.match(this.state.input)
})
.map(tag => {
return <button onClick={e => this.handleSuggestClick(tag)(e)} key={tag}>{tag}</button>
})
: null
return (
<div className='TagSelect' onClick={e => this.handleThisClick(e)}>
<div className='TagSelect-tags'>
{tagElements}
<input
type='text'
onKeyDown={e => this.handleKeyDown(e)}
ref='tagInput'
valueLink={this.linkState('input')}
placeholder='Click here to add tags'
className='TagSelect-input'
onFocus={e => this.handleInputFocus(e)}
/>
</div>
{suggestElements != null && suggestElements.length > 0
? (
<div ref='suggestTags' className='TagSelect-suggest'>
{suggestElements}
</div>
)
: null
}
</div>
)
}
}
TagSelect.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func,
suggestTags: PropTypes.array
}
TagSelect.prototype.linkState = linkState

View File

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

View File

@@ -0,0 +1,202 @@
import React, { PropTypes } 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'>
</div>
)
}
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}
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'
fontSize={config.preview.fontSize}
fontFamily={config.preview.fontFamily}
codeBlockTheme={config.preview.codeBlockTheme}
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
value={note.content}
/>
)
}
}
NoteDetail.propTypes = {
}
export default CSSModules(NoteDetail, styles)

View File

@@ -0,0 +1,109 @@
.root
absolute top bottom left right
width 100%
height 100%
.description
absolute top left right
height 80px
border-bottom $ui-border
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
.tabList
absolute left right
top 80px
box-sizing border-box
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList-item
position relative
flex 1
border-right $ui-border
.tabList-item--active
@extend .tabList-item
.tabList-item-button
border-color $brand-color
.tabList-item-button
width 100%
height 29px
navButtonColor()
outline none
border-left 4px solid transparent
.tabList-item-deleteButton
position absolute
top 5px
height 20px
right 5px
width 20px
text-align center
border none
padding 0
color transparent
background-color transparent
border-radius 2px
.tabList-plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-content
absolute top left right bottom
box-sizing border-box
height 100%
width 100%
body[data-theme="dark"]
.description
border-color $ui-dark-borderColor
.description-textarea
background-color $ui-dark-button--hover-backgroundColor
color white
.tabList
background-color $ui-button--active-backgroundColor
border-bottom-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
.tabList-item
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.tabList-item--active
border-color $ui-dark-borderColor
.tabList-item-button
border-color $brand-color
.tabList-item-button
navDarkButtonColor()
border-left 4px solid transparent
.tabList-plusButton
navDarkButtonColor()
.tabView-top
border-color $ui-dark-borderColor
.tabView-top-name
border-color $ui-dark-borderColor
color $ui-dark-text-color
background-color $ui-dark-button--hover-backgroundColor
.tabView-top-mode
border-color $ui-dark-borderColor
background-color $dark-default-button-background
color $ui-dark-inactive-text-color

View File

@@ -0,0 +1,85 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteItem.styl'
import moment from 'moment'
import _ from 'lodash'
class NoteItem extends React.Component {
constructor (props) {
super(props)
this.state = {
}
}
handleClick (e) {
this.props.onClick(e)
}
render () {
let { note, folder, storage, isActive } = this.props
let tagList = _.isArray(note.tags)
? note.tags.map((tag) => {
return (
<span styleName='bottom-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
return (
<div styleName={isActive
? 'root--active'
: 'root'
}
key={note.storage + '-' + note.key}
onClick={(e) => this.handleClick(e)}
>
<div styleName='info'>
<div styleName='info-left'>
<span styleName='info-left-folder'
style={{borderColor: folder.color}}
>
{folder.name}
<span styleName='info-left-folder-surfix'>in {storage.name}</span>
</span>
</div>
</div>
<div styleName='title'>
{note.type === 'SNIPPET_NOTE'
? <i styleName='title-icon' className='fa fa-fw fa-code'/>
: <i styleName='title-icon' className='fa fa-fw fa-file-text-o'/>
}
{note.title.trim().length > 0
? note.title
: <span styleName='title-empty'>Empty</span>
}
</div>
<div styleName='bottom'>
<i styleName='bottom-tagIcon'
className='fa fa-tags fa-fw'
/>
<div styleName='bottom-tagList'>
{tagList.length > 0
? tagList
: <span styleName='bottom-tagList-empty'>Not tagged yet</span>
}
</div>
<div styleName='bottom-time'>
{moment(note.updatedAt).fromNow()}
</div>
</div>
</div>
)
}
}
NoteItem.propTypes = {
}
export default CSSModules(NoteItem, styles)

View File

@@ -0,0 +1,179 @@
.root
position relative
border-bottom $ui-border
padding 2px 5px
user-select none
cursor pointer
transition background-color 0.15s
&:hover
background-color alpha($ui-active-color, 20%)
.root--active
@extend .root
background-color $ui-active-color
&:hover
background-color $ui-active-color
color white
.info-left-folder
.info-left-folder-surfix
.title
.title-icon
.title-empty
.bottom-tagIcon
.bottom-tagList-item
.bottom-tagList-empty
.bottom-time
color white
.bottom-tagList-item
color white
background-color transparent
.border
absolute top bottom left right
border-style solid
border-width 2px
border-color transparent
transition 0.15s
.info
height 20px
clearfix()
font-size 12px
color $ui-inactive-text-color
line-height 20px
overflow-y hidden
.info-left
float left
overflow ellipsis
.info-left-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
.info-left-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.info-right
float right
.title
height 24px
box-sizing border-box
line-height 24px
height 20px
line-height 20px
padding 0 5px 0 0
overflow ellipsis
color $ui-text-color
.title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.title-empty
font-weight normal
color $ui-inactive-text-color
.bottom
margin-top 2px
height 20px
font-size 12px
line-height 20px
overflow ellipsis
display flex
.bottom-tagIcon
vertical-align middle
color $ui-button-color
height 20px
line-height 20px
.bottom-tagList
flex 1
overflow ellipsis
line-height 20px
.bottom-tagList-item
margin 0 4px
padding 0 4px
height 20px
box-sizing border-box
border-radius 3px
vertical-align middle
border-style solid
border-color $ui-button--focus-borderColor
border-width 0 0 0 3px
background-color $ui-backgroundColor
color $ui-text-color
transition 0.15s
.bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
font-size 10px
margin-left 5px
.bottom-time
color $ui-inactive-text-color
margin-left 5px
font-size 10px
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
.root--active
@extend .root
border-color $ui-dark-borderColor
&:hover
background-color $ui-active-color
.info-left-folder
.info-left-folder-surfix
.title
.title-icon
.title-empty
.bottom-tagIcon
.bottom-tagList-item
.bottom-tagList-empty
.bottom-time
color white
.bottom-tagList-item
color white
background-color transparent
.info
color $ui-dark-inactive-text-color
.info-left-folder
color $ui-dark-text-color
.info-left-folder-surfix
color $ui-dark-inactive-text-color
.title
color $ui-dark-text-color
.title-icon
color $ui-dark-inactive-text-color
.title-empty
color $ui-dark-inactive-text-color
.tagList-empty
color $ui-dark-inactive-text-color
.bottom-tagIcon
color $ui-dark-button-color
.bottom-tagList-item
color $ui-dark-text-color
background-color $ui-dark-backgroundColor
.bottom-tagList-empty
color $ui-dark-inactive-text-color
.bottom-time
color $ui-dark-inactive-text-color

View File

@@ -0,0 +1,89 @@
import React, { PropTypes } from 'react'
import NoteItem from './NoteItem'
import _ from 'lodash'
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 { storageMap, notes, index } = this.props
let notesList = notes
.slice(0, 10 + 10 * this.state.range)
.map((note, _index) => {
let storage = storageMap[note.storage]
let folder = _.find(storage.folders, {key: note.folder})
return (
<NoteItem
note={note}
key={`${note.storage}-${note.key}`}
storage={storage}
folder={folder}
isActive={index === _index}
onClick={(e) => this.props.handleNoteClick(e, _index)}
/>
)
})
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,79 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageSection.styl'
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) => {
return (
<button styleName={filter.type === 'FOLDER' && filter.folder === folder.key && filter.storage === storage.key
? 'folderList-item--active'
: 'folderList-item'
}
style={{borderColor: folder.color}}
key={folder.key}
onClick={(e) => this.handleFolderClick(e, folder)}
>
{folder.name}
</button>
)
})
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,70 +1,90 @@
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 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'
require('!!style!css!stylus?sourceMap!../main/global.styl')
require('../lib/customMeta')
const electron = require('electron')
const { clipboard, ipcRenderer, remote } = electron
const path = require('path')
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')
}
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
let finderWindow = remote.getCurrentWindow()
if (global.process.platform === 'win32') {
finderWindow.blur()
finderWindow.hide()
}
return new window.Notification(title, options)
if (global.process.platform === 'darwin') {
Menu.sendActionToFirstResponder('hide:')
}
remote.getCurrentWindow().hide()
}
require('../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'
require('!!style!css!stylus?sourceMap!../styles/finder/index.styl')
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) {
let { filter } = this.state
filter.type = 'ALL'
this.setState({
search: '',
filter,
index: 0
})
}
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()
@@ -76,7 +96,8 @@ class FinderMain extends React.Component {
}
if (e.keyCode === 13) {
this.saveToClipboard()
this.refs.detail.saveToClipboard()
hideFinder()
e.preventDefault()
}
if (e.keyCode === 27) {
@@ -86,25 +107,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!'
})
hideFinder()
}
handleSearchChange (e) {
let { dispatch } = this.props
dispatch(searchArticle(e.target.value))
this.setState({
search: e.target.value,
index: 0
})
}
selectArticle (article) {
@@ -112,151 +121,237 @@ 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}
/>
<FinderList
ref='finderList'
activeArticle={activeArticle}
articles={articles}
dispatch={dispatch}
selectArticle={article => this.selectArticle(article)}
/>
<FinderDetail
activeArticle={activeArticle}
saveToClipboard={saveToClipboard}
/>
<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)}
/>
</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>
<button styleName={filter.type === 'ALL'
? 'result-nav-menu--active'
: 'result-nav-menu'
}
onClick={(e) => this.handleAllNotesButtonClick(e)}
><i className='fa fa-files-o fa-fw'/> All Notes</button>
<button styleName={filter.type === 'STARRED'
? 'result-nav-menu--active'
: 'result-nav-menu'
}
onClick={(e) => this.handleStarredButtonClick(e)}
><i className='fa fa-star fa-fw'/> Starred</button>
<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

View File

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

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_CREATE' || 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,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()
setClientKey(clientKey)
}
return clientKey
}
function setClientKey (newKey) {
localStorage.setItem('clientKey', newKey)
}
getClientKey()
export default {
get: getClientKey,
set: setClientKey
}

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

@@ -0,0 +1,36 @@
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',
'#FF8E00',
'#E8D252',
'#3FD941',
'#30D5C8',
'#2BA5F7',
'#B013A4'
],
FOLDER_COLOR_NAMES: [
'Razzmatazz (Red)',
'Pizazz (Orange)',
'Confetti (Yellow)',
'Emerald (Green)',
'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,4 @@
import CodeMirror from 'codemirror'
import _ from 'lodash'
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['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

@@ -1,10 +0,0 @@
const electron = require('electron')
const remote = electron.remote
const jetpack = require('fs-jetpack')
const userDataPath = remote.app.getPath('userData')
const configFile = 'config.json'
export default function fetchConfig () {
return Object.assign({}, JSON.parse(jetpack.cwd(userDataPath).read(configFile, 'utf-8')))
}

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

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

View File

@@ -1,7 +1,7 @@
var crypto = require('crypto')
const crypto = require('crypto')
const _ = require('lodash')
module.exports = function () {
var shasum = crypto.createHash('sha1')
shasum.update(((new Date()).getTime() + Math.round(Math.random()*1000)).toString())
return shasum.digest('hex')
module.exports = function (length) {
if (!_.isFinite(length)) length = 10
return crypto.randomBytes(length).toString('hex')
}

View File

@@ -1,36 +0,0 @@
function getIn (object, path) {
let stack = path.split('.')
while (stack.length > 1) {
object = object[stack.shift()]
}
return object[stack.shift()]
}
function updateIn (object, path, value) {
let current = object
let stack = path.split('.')
while (stack.length > 1) {
current = current[stack.shift()]
}
current[stack.shift()] = value
return object
}
function setPartialState (component, path, value) {
component.setState(
updateIn(component.state, path, value))
}
export default function linkState (path) {
return {
value: getIn(this.state, path),
requestChange: setPartialState.bind(null, this, path)
}
}
export function linkState2 (el, path) {
return {
value: getIn(el.state, path),
requestChange: setPartialState.bind(null, el, path)
}
}

View File

@@ -1,7 +1,19 @@
import markdownit from 'markdown-it'
import emoji from 'markdown-it-emoji'
import math from 'markdown-it-math'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
import _ from 'lodash'
const katex = window.katex
function createGutter (str) {
let lc = (str.match(/\n/g) || []).length
let lines = []
for (let i = 1; i <= lc; i++) {
lines.push('<span>' + i + '</span>')
}
return '<span class="lineNumber">' + lines.join('') + '</span>'
}
var md = markdownit({
typographer: true,
@@ -11,10 +23,18 @@ var md = markdownit({
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value
return '<pre class="hljs">' +
createGutter(str) +
'<code>' +
hljs.highlight(lang, str).value +
'</code></pre>'
} catch (e) {}
}
return str
return '<pre class="hljs">' +
createGutter(str) +
'<code>' +
str.replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\"/g, '&quot;') +
'</code></pre>'
}
})
md.use(emoji, {
@@ -22,27 +42,127 @@ md.use(emoji, {
})
md.use(math, {
inlineRenderer: function (str) {
return `<span class='math'>${str}</span>`
let output = ''
try {
output = katex.renderToString(str.trim())
} catch (err) {
output = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
return `<div class='math'>${str}</div>`
let output = ''
try {
output = katex.renderToString(str.trim(), {displayMode: true})
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
return output
}
})
md.use(require('markdown-it-checkbox'))
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,756 +0,0 @@
const modes = [
// Major
{
name: 'text',
label: 'Plain text',
mode: 'text'
},
{
name: 'markdown',
label: 'Markdown',
alias: ['md'],
mode: 'markdown'
},
{
name: 'javascript',
label: 'JavaScript',
alias: ['js', 'jscript', 'babel', 'es'],
mode: 'javascript'
},
{
name: 'html',
label: 'HTML',
alias: [],
mode: 'html'
},
{
name: 'css',
label: 'CSS',
alias: ['cascade', 'stylesheet'],
mode: 'css'
},
{
name: 'php',
label: 'PHP',
alias: [],
mode: 'php'
},
{
name: 'python',
label: 'Python',
alias: ['py'],
mode: 'python'
},
{
name: 'ruby',
label: 'Ruby',
alias: ['rb'],
mode: 'ruby'
},
{
name: 'java',
label: 'Java',
alias: [],
mode: 'java'
},
{
name: 'c',
label: 'C',
alias: ['c', 'h', 'clang', 'clang'],
mode: 'c_cpp'
},
{
name: 'cpp',
label: 'C++',
alias: ['cc', 'cpp', 'cxx', 'hh', 'c++', 'cplusplus'],
mode: 'c_cpp'
},
{
name: 'csharp',
label: 'C#',
alias: ['cs', 'c#'],
mode: 'csharp'
},
{
name: 'swift',
label: 'Swift',
alias: [],
mode: 'swift'
},
{
name: 'golang',
label: 'Go',
alias: ['go'],
mode: 'golang'
},
// Minor
{
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: '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: '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: '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_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: '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: '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: '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: 'r',
label: 'R',
alias: ['rlang'],
mode: 'r'
},
{
name: 'rdoc',
label: 'RDoc',
alias: [],
mode: 'rdoc'
},
{
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: '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,7 +0,0 @@
const electron = require('electron')
const shell = electron.shell
export default function (e) {
shell.openExternal(e.currentTarget.href)
e.preventDefault()
}

View File

@@ -1,40 +0,0 @@
'use strict'
var _ = require('lodash')
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const TAG_FILTER = 'TAG_FILTER'
export default function search (articles, search) {
let filters = search.split(' ').map(key => key.trim()).filter(key => key.length > 0 && !key.match(/^#$/)).map(key => {
if (key.match(/^in:.+$/)) {
return {type: FOLDER_FILTER, value: key.match(/^in:(.+)$/)[1]}
}
if (key.match(/^#(.+)/)) {
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
}
return {type: TEXT_FILTER, value: key}
})
// 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)
if (textFilters.length > 0) {
articles = textFilters.reduce((articles, textFilter) => {
return articles.filter(article => {
return article.title.match(new RegExp(textFilter.value, 'i')) || article.content.match(new RegExp(textFilter.value, 'i'))
})
}, articles)
}
if (tagFilters.length > 0) {
articles = tagFilters.reduce((articles, tagFilter) => {
return articles.filter(article => {
return _.find(article.Tags, tag => tag.name.match(new RegExp(tagFilter.value, 'i')))
})
}, articles)
}
return articles
}

View File

@@ -0,0 +1,24 @@
.root
absolute top bottom right
border-width 1px 0
border-style solid
border-color $ui-borderColor
.empty
height 320px
display flex
align-items center
.empty-message
width 100%
font-size 42px
line-height 72px
text-align center
color $ui-inactive-text-color
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
.empty-message
color $ui-dark-inactive-text-color

View File

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

View File

@@ -0,0 +1,132 @@
.root
position relative
border solid 1px transparent
line-height 34px
vertical-align middle
border-radius 2px
transition 0.15s
user-select none
&:hover
background-color white
border-color $ui-borderColor
.root--search, .root--focus
@extend .root
background-color white
border-color $ui-input--focus-borderColor
&:hover
background-color white
border-color $ui-input--focus-borderColor
.idle
position relative
cursor pointer
.idle-label
absolute left top
padding 0 0 0 5px
right 20px
overflow ellipsis
.idle-label-name
border-left solid 4px transparent
padding 2px 5px
.idle-label-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
.idle-caret
absolute right top
height 34px
width 20px
line-height 34px
.search
absolute top left right bottom
line-height 34px
.search-input
vertical-align middle
position relative
top -2px
outline none
border none
height 20px
line-height 20px
background-color transparent
padding 0 10px
.search-optionList
position fixed
max-height 450px
overflow auto
z-index 200
background-color white
border-radius 2px
box-shadow 2px 2px 10px gray
.search-optionList-item
height 34px
width 250px
box-sizing border-box
padding 0 5px
overflow ellipsis
cursor pointer
&:hover
background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
.search-optionList-item-name
border-left solid 4px transparent
padding 2px 5px
.search-optionList-item-name-surfix
font-size 10px
color $ui-inactive-text-color
margin-left 5px
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
background-color $ui-dark-button--hover-backgroundColor
box-shadow 2px 2px 10px black
.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,305 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './MarkdownNoteDetail.styl'
import MarkdownEditor from 'browser/components/MarkdownEditor'
import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdown'
import StatusBar from '../StatusBar'
import _ from 'lodash'
const electron = require('electron')
const { remote } = electron
const { Menu, MenuItem, dialog } = remote
class MarkdownNoteDetail extends React.Component {
constructor (props) {
super(props)
this.state = {
isMovingNote: false,
note: Object.assign({
title: '',
content: ''
}, props.note)
}
this.dispatchTimer = null
}
focus () {
this.refs.content.focus()
}
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)
}, () => {
this.refs.content.reload()
this.refs.tags.reset()
})
}
}
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
title = markdown.strip(title)
return title
}
handleChange (e) {
let { note } = this.state
note.content = this.refs.content.value
note.tags = this.refs.tags.value
note.title = this.findTitle(note.content)
note.updatedAt = new Date()
this.setState({
note
}, () => {
this.save()
})
}
save () {
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleDeleteMenuClick (e) {
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 { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
}
handleDeleteKeyDown (e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
}
render () {
let { data, config } = this.props
let { note } = this.state
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
>
<div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
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)}
disabled
>
<i className='fa fa-share-alt fa-fw' />
<span styleName='info-right-button-tooltip'
style={{right: 20}}
>Share Note</span>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v' />
<span styleName='info-right-button-tooltip'
style={{right: 5}}
>More Options</span>
</button>
</div>
</div>
<div styleName='body'>
<MarkdownEditor
ref='content'
styleName='body-noteEditor'
config={config}
value={this.state.note.content}
onChange={(e) => this.handleChange(e)}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
</div>
<StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])}
/>
</div>
)
}
}
MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(MarkdownNoteDetail, styles)

View File

@@ -0,0 +1,102 @@
$info-height = 75px
.root
absolute top right bottom
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-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 .info-right-button-tooltip
opacity 1
&:focus
border-color $ui-button--focus-borderColor
.info-right-button-tooltip
tooltip()
position fixed
top 45px
padding 5px
opacity 0
border-radius 2px
.body
absolute left right
top $info-height
bottom $statusBar-height
.body-noteEditor
absolute top bottom left right
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
.info
border-color $ui-dark-borderColor
background-color $ui-dark-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-button
navDarkButtonColor()
border-color $ui-dark-borderColor
&:active
border-color $ui-dark-button--focus-borderColor
&:hover .info-right-button-tooltip
opacity 1
&:focus
border-color $ui-button--focus-borderColor
.info-right-button-tooltip
darkTooltip()

View File

@@ -0,0 +1,653 @@
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 { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror'
import SnippetTab from './SnippetTab'
import StatusBar from '../StatusBar'
import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager'
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, 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))
})
}
}
componentWillReceiveProps (nextProps) {
if (nextProps.note.key !== this.props.note.key) {
if (this.saveQueue != null) this.saveNow()
let nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
})
this.setState({
snippetIndex: 0,
note: nextNote
}, () => {
let { snippets } = this.state.note
snippets.forEach((snippet, index) => {
this.refs['code-' + index].reload()
})
this.refs.tags.reset()
})
}
}
componentWillUnmount () {
if (this.saveQueue != null) this.saveNow()
}
findTitle (value) {
let splitted = value.split('\n')
let title = null
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.match(/^# .+/)) {
title = trimmedLine.substring(1, trimmedLine.length).trim()
break
}
}
if (title == null) {
for (let i = 0; i < splitted.length; i++) {
let trimmedLine = splitted[i].trim()
if (trimmedLine.length > 0) {
title = trimmedLine
break
}
}
if (title == null) {
title = ''
}
}
return title
}
handleChange (e) {
let { note } = this.state
note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
note.title = this.findTitle(note.description)
this.setState({
note
}, () => {
this.save()
})
}
save () {
clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => {
this.saveNow()
}, 1000)
}
saveNow () {
let { note, dispatch } = this.props
clearTimeout(this.saveQueue)
this.saveQueue = null
dataApi
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
})
}
handleFolderChange (e) {
let { note } = this.state
let value = this.refs.folder.value
let splitted = value.split('-')
let newStorageKey = splitted.shift()
let newFolderKey = splitted.shift()
dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => {
this.setState({
isMovingNote: true,
note: Object.assign({}, newNote)
}, () => {
let { dispatch, location } = this.props
dispatch({
type: 'MOVE_NOTE',
originNote: note,
note: newNote
})
hashHistory.replace({
pathname: location.pathname,
query: {
key: newNote.storage + '-' + newNote.key
}
})
this.setState({
isMovingNote: false
})
})
})
}
handleStarButtonClick (e) {
let { note } = this.state
note.isStarred = !note.isStarred
this.setState({
note
}, () => {
this.save()
})
}
exportAsFile () {
}
handleShareButtonClick (e) {
let menu = new Menu()
menu.append(new MenuItem({
label: 'Export as a File',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.append(new MenuItem({
label: 'Export to Web',
disabled: true,
click: (e) => this.handlePreferencesButtonClick(e)
}))
menu.popup(remote.getCurrentWindow())
}
handleContextButtonClick (e) {
context.popup([{
label: 'Delete',
click: (e) => this.handleDeleteMenuClick(e)
}])
}
handleDeleteMenuClick (e) {
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 { note, dispatch } = this.props
dataApi
.deleteNote(note.storage, note.key)
.then((data) => {
let dispatchHandler = () => {
dispatch({
type: 'DELETE_NOTE',
storageKey: data.storageKey,
noteKey: data.noteKey
})
}
ee.once('list:moved', dispatchHandler)
ee.emit('list:next')
})
}
}
handleTabPlusButtonClick (e) {
this.addSnippet()
}
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)
}
}
}
deleteSnippetByIndex (index) {
let snippets = this.state.note.snippets.slice()
snippets.splice(index, 1)
this.state.note.snippets = snippets
let snippetIndex = this.state.snippetIndex >= snippets.length
? snippets.length - 1
: this.state.snippetIndex
this.setState({
note: this.state.note,
snippetIndex
})
}
renameSnippetByIndex (index, name) {
let snippets = this.state.note.snippets.slice()
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({
note: this.state.note
}, () => {
this.save()
})
}
handleModeOptionClick (index, name) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].mode = name
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
handleCodeChange (index) {
return (e) => {
let snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
this.state.note.snippets = snippets
this.setState({
note: this.state.note
}, () => {
this.save()
})
}
}
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 { data, config } = this.props
let { note } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
let tabList = note.snippets.map((snippet, index) => {
let isActive = this.state.snippetIndex === index
return <SnippetTab
key={index}
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 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'
value={snippet.content}
config={config}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
value={snippet.content}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
/>
}
</div>
})
return (
<div className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
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)}
disabled
>
<i className='fa fa-share-alt fa-fw'/>
<span styleName='info-right-button-tooltip'
style={{right: 20}}
>Share Note</span>
</button>
<button styleName='info-right-button'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-ellipsis-v'/>
<span styleName='info-right-button-tooltip'
style={{right: 5}}
>More Options</span>
</button>
</div>
</div>
<div styleName='body'>
<div styleName='description'>
<textarea
style={{
fontFamily: config.preview.fontFamily,
fontSize: parseInt(config.preview.fontSize, 10)
}}
ref='description'
placeholder='Description...'
value={this.state.note.description}
onChange={(e) => this.handleChange(e)}
/>
</div>
<div styleName='tabList'>
<div styleName='list'>
{tabList}
</div>
<button styleName='plusButton'
onClick={(e) => this.handleTabPlusButtonClick(e)}
>
<i className='fa fa-plus'/>
</button>
</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'])}
/>
</div>
)
}
}
SnippetNoteDetail.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
note: PropTypes.shape({
}),
style: PropTypes.shape({
left: PropTypes.number
}),
ignorePreviewPointerEvents: PropTypes.bool
}
export default CSSModules(SnippetNoteDetail, styles)

View File

@@ -0,0 +1,170 @@
$info-height = 75px
.root
absolute top bottom right
border-width 0 0 1px
border-style solid
border-color $ui-borderColor
.info
absolute top left right
height $info-height
border-bottom $ui-border
background-color $ui-backgroundColor
.info-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 .info-right-button-tooltip
opacity 1
&:focus
border-color $ui-button--focus-borderColor
.info-right-button-tooltip
tooltip()
position fixed
top 45px
padding 5px
opacity 0
.body
absolute left right
top $info-height
bottom $statusBar-height
.body .description
absolute top left right
height 80px
border-bottom $ui-border
.body .description textarea
display block
height 100%
width 100%
resize none
border none
padding 10px
line-height 1.6
.tabList
absolute left right
top 80px
height 30px
border-bottom $ui-border
display flex
background-color $ui-backgroundColor
.tabList .list
flex 1
display flex
overflow hidden
border-right $ui-border
.tabList .plusButton
navButtonColor()
width 30px
.tabView
absolute left right bottom
top 110px
.tabView-content
absolute top left right bottom
.override
absolute bottom left
height 23px
z-index 1
button
navButtonColor()
height 24px
border-width 0 1px 0 0
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
.info
border-bottom-color $ui-dark-borderColor
background-color $ui-dark-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-button
navDarkButtonColor()
border-color $ui-dark-borderColor
&:active
border-color $ui-dark-button--focus-borderColor
&:hover .info-right-button-tooltip
opacity 1
&:focus
border-color $ui-button--focus-borderColor
.info-right-button-tooltip
darkTooltip()
.body .description
border-bottom-color $ui-dark-borderColor
.body .description textarea
background-color $ui-dark-button--hover-backgroundColor
color white
.tabList
background-color $ui-button--active-backgroundColor
border-bottom-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
.tabList .list
border-color $ui-dark-borderColor
.tabList .plusButton
navDarkButtonColor()
.override
button
border-color $ui-dark-borderColor

View File

@@ -0,0 +1,131 @@
import React, { PropTypes } 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)}
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,102 @@
.root
position relative
flex 1
overflow hidden
border-right $ui-border
&:last-child
border-right none
&: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
.button
border-color $brand-color
.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
&:active, &:active:hover
border-color $brand-color
.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 white
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-dark-text-color
border-color $ui-dark-borderColor
.button
border-color $brand-color
&: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 white
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 white
background-color $ui-dark-button--hover-backgroundColor
&:active
color $ui-dark-button--active-color
&:active, &:active:hover
color $ui-dark-button--active-color
.input
background-color $ui-dark-button--hover-backgroundColor
color white

View File

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

View File

@@ -0,0 +1,27 @@
.root
position relative
padding 0
&:hover
.icon
transform rotate(-72deg)
.tooltip
opacity 1
.root--active
@extend .root
color $brand-color
&:hover
color $brand-color !important
.icon
transform rotate(-72deg)
.icon
transition transform 0.15s
.tooltip
tooltip()
position fixed
top 45px
right 65px
padding 5px
opacity 0
border-radius 2px

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,362 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import moment from 'moment'
import _ from 'lodash'
import {
switchFolder,
updateArticle,
// cacheArticle,
// saveArticle,
// uncacheArticle
} from '../../actions'
import linkState from 'browser/lib/linkState'
import TagSelect from 'browser/components/TagSelect'
import ModeSelect from 'browser/components/ModeSelect'
import ShareButton from './ShareButton'
import { openModal, isModalOpen } from 'browser/lib/modal'
import DeleteArticleModal from '../../modal/DeleteArticleModal'
import ArticleEditor from './ArticleEditor'
const electron = require('electron')
const ipc = electron.ipcRenderer
// const remote = electron.remote
// const { Menu, MenuItem } = remote
// const othersMenu = new Menu()
// othersMenu.append(new MenuItem({
// label: 'Delete Post',
// click: function () {
// remote.getCurrentWebContents().send('detail-delete')
// }
// }))
// othersMenu.append(new MenuItem({
// label: 'Discard Change',
// click: function (item) {
// remote.getCurrentWebContents().send('detail-uncache')
// }
// }))
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const tagSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='155' y='50' fill={BRAND_COLOR} fontSize='24'>Attach some tags here!</text>
<svg x='0' y='-15'>
<path fill='white' d='M15.5,22.2c77.8-0.7,155.6-1.3,233.5-2c22.2-0.2,44.4-0.4,66.6-0.6c1.9,0,1.9-3,0-3
c-77.8,0.7-155.6,1.3-233.5,2c-22.2,0.2-44.4,0.4-66.6,0.6C13.6,19.2,13.6,22.2,15.5,22.2L15.5,22.2z'/>
<path fill='white' d='M130.8,25c-5.4,6.8-10.3,14-14.6,21.5c-0.8,1.4,1.2,3.2,2.4,1.8c1-1.2,2-2.4,3.1-3.7c1.2-1.5-0.9-3.6-2.1-2.1
c-1,1.2-2,2.4-3.1,3.7c0.8,0.6,1.6,1.2,2.4,1.8c4.2-7.3,8.9-14.3,14.2-20.9C134.1,25.6,132,23.4,130.8,25L130.8,25z'/>
<path fill='white' d='M132.6,22.1c8.4,5.9,16.8,11.9,25.2,17.8c1.6,1.1,3.1-1.5,1.5-2.6c-8.4-5.9-16.8-11.9-25.2-17.8
C132.5,18.4,131,21,132.6,22.1L132.6,22.1z'/>
<path fill='white' d='M132.9,18.6c0.4,6.7-0.7,13.3-3.5,19.3c-1.5,3.1-3.9,6.4-3.1,10c0.7,3.1,3.4,4.4,6.2,5.5
c5.1,2.1,10.5,3.1,16.1,3.2c1.9,0,1.9-3,0-3c-4.7-0.1-9.2-0.8-13.6-2.4c-3-1.1-6.2-1.9-5.4-6.6c0.4-2,2-4.1,2.8-5.9
c2.9-6.3,4-13.1,3.6-20.1C135.8,16.7,132.8,16.7,132.9,18.6L132.9,18.6z'/>
</svg>
</svg>
)
const modeSelectTutorialElement = (
<svg width='500' height='500' className='tutorial'>
<text x='195' y='130' fill={BRAND_COLOR} fontSize='24'>Select code syntax!!</text>
<svg x='300' y='0'>
<path fill='white' d='M99.9,58.8c-14.5-0.5-29-2.2-43.1-5.6c-12.3-2.9-27.9-6.4-37.1-15.5C7.9,26,28.2,18.9,37,16.7
c13.8-3.5,28.3-4.7,42.4-5.8c29.6-2.2,59.3-1.7,89-1c3,0.1,7.5-0.6,10.2,0.6c3.1,1.4,3.1,5.3,3.3,8.1c0.3,5.2-0.2,10.7-2.4,15.4
c-4.4,9.6-18.4,14.7-27.5,18.1c-27.1,10.1-56.7,12.8-85.3,15.6c-1.9,0.2-1.9,3.2,0,3c29.3-2.9,59.8-5.6,87.5-16.2
c9.6-3.7,22.8-8.7,27.7-18.4c2.3-4.6,3.2-9.9,3.2-15c0-3.6,0-9.4-2.9-12c-1.9-1.7-4.7-1.8-7.1-2c-4.8-0.2-9.6-0.2-14.4-0.3
c-8.7-0.2-17.5-0.3-26.2-0.4C116.7,6.3,99,6.5,81.3,7.8c-15.8,1.1-32.1,2.3-47.4,6.6c-7.7,2.2-22.1,6.9-20.9,17.4
c0.6,5.4,5.6,9.4,9.9,12.1c6.7,4.3,14.4,6.9,22,9.2c17.8,5.4,36.4,8,54.9,8.6C101.8,61.8,101.8,58.8,99.9,58.8L99.9,58.8z'/>
<path fill='white' d='M11.1,67.8c9.2-6.1,18.6-11.9,28.2-17.2c-0.7-0.3-1.5-0.6-2.2-0.9c0.9,5.3,0.7,10.3-0.5,15.5
c-0.4,1.9,2.4,2.7,2.9,0.8c1.4-5.7,1.5-11.3,0.5-17.1c-0.2-1-1.4-1.3-2.2-0.9c-9.7,5.3-19.1,11.1-28.2,17.2
C8,66.3,9.5,68.9,11.1,67.8L11.1,67.8z'/>
<path fill='white' d='M31.5,52.8C23.4,68.9,0.2,83.2,7.9,104c0.7,1.8,3.6,1,2.9-0.8C3.6,83.7,26.4,69.7,34.1,54.3
C35,52.6,32.4,51.1,31.5,52.8L31.5,52.8z'/>
</svg>
</svg>
)
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
this.saveHandler = e => {
if (isModalOpen()) return true
this.handleSaveButtonClick()
}
this.deleteHandler = e => {
if (isModalOpen()) return true
this.handleDeleteButtonClick()
}
this.uncacheHandler = e => {
if (isModalOpen()) return true
this.handleUncache()
}
this.titleHandler = e => {
if (isModalOpen()) return true
if (this.refs.title) {
this.focusTitle()
}
}
this.editHandler = e => {
if (isModalOpen()) return true
if (this.refs.editor) this.refs.editor.switchEditMode()
}
this.state = {
article: Object.assign({content: ''}, props.activeArticle),
openShareDropdown: false
}
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
this.shareDropdownInterceptor = e => {
e.stopPropagation()
}
// ipc.on('detail-save', this.saveHandler)
ipc.on('detail-delete', this.deleteHandler)
ipc.on('detail-uncache', this.uncacheHandler)
ipc.on('detail-title', this.titleHandler)
ipc.on('detail-edit', this.editHandler)
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
// ipc.removeListener('detail-save', this.saveHandler)
ipc.removeListener('detail-delete', this.deleteHandler)
ipc.removeListener('detail-uncache', this.uncacheHandler)
ipc.removeListener('detail-title', this.titleHandler)
ipc.removeListener('detail-edit', this.editHandler)
}
componentDidUpdate (prevProps, prevState) {
if (this.props.activeArticle == null || prevProps.activeArticle == null || this.props.activeArticle.key !== prevProps.activeArticle.key) {
if (this.refs.editor) this.refs.editor.resetCursorPosition()
}
if (prevProps.activeArticle == null && this.props.activeArticle) {
}
}
renderEmpty () {
return (
<div className='ArticleDetail empty'>
<div className='ArticleDetail-empty-box'>
<div className='ArticleDetail-empty-box-message'>{OSX ? 'Command(⌘)' : 'Ctrl(^)'} + N<br/>to create a new post</div>
</div>
</div>
)
}
handleOthersButtonClick (e) {
this.deleteHandler()
}
handleFolderKeyChange (e) {
let { dispatch, activeArticle, status, folders } = this.props
let article = Object.assign({}, activeArticle, {
FolderKey: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
let targetFolderKey = e.target.value
if (status.targetFolders.length > 0) {
let targetFolder = _.findWhere(folders, {key: targetFolderKey})
dispatch(switchFolder(targetFolder.name))
}
}
handleTitleChange (e) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
title: e.target.value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleTagsChange (newTag, tags) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
tags: tags,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
handleModeChange (value) {
let { dispatch, activeArticle } = this.props
let article = Object.assign({}, activeArticle, {
mode: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
this.switchEditMode()
}
handleContentChange (value) {
let { dispatch, activeArticle } = this.props
if (activeArticle.content !== value) {
let article = Object.assign({}, activeArticle, {
content: value,
updatedAt: new Date()
})
dispatch(updateArticle(article))
}
}
handleDeleteButtonClick (e) {
if (this.props.activeArticle) {
openModal(DeleteArticleModal, {articleKey: this.props.activeArticle.key})
}
}
handleTitleKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.refs.mode.handleIdleSelectClick()
}
}
handleModeSelectKeyDown (e) {
if (e.keyCode === 9 && !e.shiftKey) {
e.preventDefault()
this.switchEditMode()
}
if (e.keyCode === 9 && e.shiftKey) {
e.preventDefault()
this.focusTitle()
}
if (e.keyCode === 27) {
this.focusTitle()
}
}
switchEditMode () {
this.refs.editor.switchEditMode()
}
focusTitle () {
if (this.refs.title) {
let titleEl = ReactDOM.findDOMNode(this.refs.title)
titleEl.focus()
titleEl.select()
}
}
render () {
let { folders, status, tags, activeArticle, modified, user } = this.props
if (activeArticle == null) return this.renderEmpty()
let folderOptions = folders.map(folder => {
return (
<option key={folder.key} value={folder.key}>{folder.name}</option>
)
})
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
return (
<div tabIndex='4' className='ArticleDetail'>
<div className='ArticleDetail-info'>
<div className='ArticleDetail-info-row'>
<select
className='ArticleDetail-info-folder'
value={activeArticle.FolderKey}
onChange={e => this.handleFolderKeyChange(e)}
>
{folderOptions}
</select>
<span className='ArticleDetail-info-status'
children={
isUnsaved
? <span> <span className='unsaved-mark'></span> Unsaved</span>
: `Created : ${moment(activeArticle.createdAt).format('YYYY/MM/DD')} Updated : ${moment(activeArticle.updatedAt).format('YYYY/MM/DD')}`
}
/>
<div className='ArticleDetail-info-control'>
{/*<div className={'ArticleDetail-info-control-save' + (!isUnsaved ? ' hide' : '')}>
<button
onClick={e => this.handleSaveButtonClick(e)}
className='ArticleDetail-info-control-save-button'
disabled={!isUnsaved}
>
<i className='fa fa-fw fa-save'/>&nbsp;Save
<span className='tooltip' children={`Save Post (${OSX ? '⌘' : '^'} + S)`}/>
</button>
</div>*/}
<ShareButton
article={activeArticle}
user={user}
/>
<button className='ArticleDetail-info-control-delete-button' onClick={e => this.handleOthersButtonClick(e)}>
<i className='fa fa-fw fa-trash'/>
<span className='tooltip' children={`Delete Post (^ + Del)`}/>
</button>
</div>
</div>
<div className='ArticleDetail-info-row2'>
<TagSelect
tags={activeArticle.tags}
onChange={(tags, tag) => this.handleTagsChange(tags, tag)}
suggestTags={tags}
/>
{status.isTutorialOpen ? tagSelectTutorialElement : null}
</div>
</div>
<div className='ArticleDetail-panel'>
<div className='ArticleDetail-panel-header'>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder='(Untitled)'
ref='title'
value={activeArticle.title}
onChange={e => this.handleTitleChange(e)}
/>
</div>
<ModeSelect
ref='mode'
onChange={e => this.handleModeChange(e)}
onKeyDown={e => this.handleModeSelectKeyDown(e)}
value={activeArticle.mode}
className='ArticleDetail-panel-header-mode'
/>
{status.isTutorialOpen ? modeSelectTutorialElement : null}
</div>
<ArticleEditor
ref='editor'
article={activeArticle}
onChange={content => this.handleContentChange(content)}
/>
</div>
</div>
)
}
}
ArticleDetail.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape(),
tags: PropTypes.array,
user: PropTypes.shape(),
folders: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}
ArticleDetail.prototype.linkState = linkState

View File

@@ -1,207 +0,0 @@
import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment'
import { switchArticle } from '../actions'
import FolderMark from 'browser/components/FolderMark'
import TagLink from './TagLink'
import _ from 'lodash'
const electron = require('electron')
const remote = electron.remote
const ipc = electron.ipcRenderer
export default class ArticleList extends React.Component {
constructor (props) {
super(props)
this.focusHandler = e => this.focus()
}
componentDidMount () {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ipc.on('list-focus', this.focusHandler)
this.focus()
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ipc.removeListener('list-focus', this.focusHandler)
}
componentDidUpdate () {
let { articles, activeArticle } = this.props
var index = articles.indexOf(activeArticle)
var el = ReactDOM.findDOMNode(this)
var li = el.querySelectorAll('.ArticleList>div')[index]
if (li == null) {
return
}
var overflowBelow = el.clientHeight + el.scrollTop < li.offsetTop + li.clientHeight
if (overflowBelow) {
el.scrollTop = li.offsetTop + li.clientHeight - el.clientHeight
}
var overflowAbove = el.scrollTop > li.offsetTop
if (overflowAbove) {
el.scrollTop = li.offsetTop
}
}
focus () {
ReactDOM.findDOMNode(this).focus()
}
// 移動ができなかったらfalseを返す:
selectPriorArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) - 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
selectNextArticle () {
let { articles, activeArticle, dispatch } = this.props
let targetIndex = articles.indexOf(activeArticle) + 1
let targetArticle = articles[targetIndex]
if (targetArticle != null) {
dispatch(switchArticle(targetArticle.key))
return true
}
return false
}
handleArticleClick (article) {
let { dispatch } = this.props
return function (e) {
dispatch(switchArticle(article.key))
}
}
handleArticleListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('top-new-post')
}
if (e.keyCode === 65 && e.shiftKey) {
e.preventDefault()
remote.getCurrentWebContents().send('nav-new-folder')
}
if (e.keyCode === 68) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-delete')
}
if (e.keyCode === 84) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-title')
}
if (e.keyCode === 69) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-edit')
}
if (e.keyCode === 83) {
e.preventDefault()
remote.getCurrentWebContents().send('detail-save')
}
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorArticle()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextArticle()
}
}
render () {
let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => {
let modifiedArticle = _.findWhere(modified, {key: article.key})
let originalArticle = article
if (modifiedArticle) {
article = Object.assign({}, article)
}
let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.slice().map(tag => {
return (<TagLink key={tag} tag={tag}/>)
})
: (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey})
let folderChanged = originalArticle.FolderKey !== article.FolderKey
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
let title = article.title.trim().length === 0
? <small>(Untitled)</small>
: article.title
return (
<div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'ArticleList-item' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='ArticleList-item-top'>
{folder != null
? folderChanged
? <span className='folderName'>
<FolderMark color={originalFolder.color}/>{originalFolder.name}
->
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span className='folderName'>
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span><FolderMark color={-1}/>Unknown</span>
}
<span className='updatedAt'
children={
modifiedArticle != null
? <span><span className='unsaved-mark'></span> Unsaved</span>
: moment(article.updatedAt).fromNow()
}
/>
</div>
<div className='ArticleList-item-middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div>
<div className='ArticleList-item-middle2'>
<pre><code children={article.content.trim().length === 0 ? '(Empty content)' : article.content.substring(0, 50)}/></pre>
</div>
<div className='ArticleList-item-bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
</div>
</div>
<div className='divider'></div>
</div>
)
})
return (
<div tabIndex='3' onKeyDown={e => this.handleArticleListKeyDown(e)} className='ArticleList'>
{articleElements}
</div>
)
}
}
ArticleList.propTypes = {
dispatch: PropTypes.func,
folders: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape()
}

View File

@@ -1,202 +0,0 @@
import React, { PropTypes } from 'react'
import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
import { openModal, isModalOpen } from 'browser/lib/modal'
import FolderMark from 'browser/components/FolderMark'
import Preferences from '../modal/Preferences'
import CreateNewFolder from '../modal/CreateNewFolder'
import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
const ipc = require('electron').ipcRenderer
const BRAND_COLOR = '#18AF90'
const OSX = global.process.platform === 'darwin'
const preferenceTutorialElement = (
<svg width='300' height='300' className='tutorial'>
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
<svg x='-30' y='-270' width='400' height='400'>
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
</svg>
</svg>
)
const newFolderTutorialElement = (
<svg width='800' height='500' className='tutorial'>
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
<svg x='50' y='10' width='300' height='400'>
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
</svg>
</svg>
)
export default class ArticleNavigator extends React.Component {
constructor (props) {
super(props)
this.newFolderHandler = e => {
if (isModalOpen()) return true
this.handleNewFolderButton(e)
}
}
componentDidMount () {
ipc.on('nav-new-folder', this.newFolderHandler)
}
componentWillUnmount () {
ipc.removeListener('nav-new-folder', this.newFolderHandler)
}
handlePreferencesButtonClick (e) {
openModal(Preferences)
}
handleNewFolderButton (e) {
let { user } = this.props
openModal(CreateNewFolder, {user: user})
}
handleFolderButtonClick (name) {
return e => {
let { dispatch } = this.props
dispatch(switchFolder(name))
}
}
handleAllFoldersButtonClick (e) {
let { dispatch } = this.props
dispatch(setSearchFilter(''))
}
handleUnsavedItemClick (article) {
let { dispatch } = this.props
return e => {
let { articles } = this.props
let isInArticleList = articles.some(_article => _article.key === article.key)
if (!isInArticleList) dispatch(clearSearch())
dispatch(switchArticle(article.key))
}
}
handleUncacheButtonClick (article) {
let { dispatch } = this.props
return e => {
dispatch(uncacheArticle(article.key))
}
}
handleSaveAllClick (e) {
let { dispatch } = this.props
dispatch(saveAllArticles())
}
render () {
let { status, user, folders, allArticles, modified, activeArticle } = this.props
let { targetFolders } = status
if (targetFolders == null) targetFolders = []
let modifiedElements = modified.map(modifiedArticle => {
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
if (originalArticle == null) return false
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
let className = 'ArticleNavigator-unsaved-list-item'
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
return (
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
<div className='ArticleNavigator-unsaved-list-item-label'>
<ModeIcon mode={combinedArticle.mode}/>&nbsp;
{combinedArticle.title.trim().length > 0
? combinedArticle.title
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
</div>
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
</div>
)
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
let hasModified = modifiedElements.length > 0
let folderElememts = folders.map((folder, index) => {
let isActive = findWhere(targetFolders, {key: folder.key})
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
return (
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
</button>
)
})
return (
<div tabIndex='1' className='ArticleNavigator'>
<div className='userInfo'>
<div className='userProfileName'>{user.name}</div>
<div className='userName'>localStorage</div>
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
<i className='fa fa-fw fa-chevron-down'/>
<span className='tooltip'>Preferences</span>
</button>
{status.isTutorialOpen ? preferenceTutorialElement : null}
</div>
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
<div className='ArticleNavigator-unsaved-list'>
{modifiedElements}
</div>
<div className='ArticleNavigator-unsaved-control'>
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
</div>
</div>*/}
<div className={'ArticleNavigator-folders expand'}>
{status.isTutorialOpen ? newFolderTutorialElement : null}
<div className='ArticleNavigator-folders-header'>
<div className='title'>Folders</div>
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
<i className='fa fa-fw fa-plus'/>
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
</button>
</div>
<div className='folderList'>
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
{folderElememts}
</div>
</div>
</div>
)
}
}
ArticleNavigator.propTypes = {
dispatch: PropTypes.func,
status: PropTypes.shape({
folderId: PropTypes.number
}),
user: PropTypes.object,
folders: PropTypes.array,
allArticles: PropTypes.array,
articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape({
key: PropTypes.string
})
}

View File

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

View File

@@ -1,18 +0,0 @@
import React, { PropTypes } from 'react'
import store from '../store'
import { setTagFilter } from '../actions'
export default class TagLink extends React.Component {
handleClick (e) {
store.dispatch(setTagFilter(this.props.tag))
}
render () {
return (
<a onClick={e => this.handleClick(e)}>{this.props.tag}</a>
)
}
}
TagLink.propTypes = {
tag: PropTypes.string
}

View File

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

View File

@@ -1,241 +0,0 @@
import React, { PropTypes} from 'react'
import { connect } from 'react-redux'
import ReactDOM from 'react-dom'
import { toggleTutorial } from '../actions'
import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './ArticleTopBar'
import ArticleList from './ArticleList'
import ArticleDetail from './ArticleDetail'
import _ from 'lodash'
import { isModalOpen, closeModal } from 'browser/lib/modal'
const electron = require('electron')
const remote = electron.remote
const TEXT_FILTER = 'TEXT_FILTER'
const FOLDER_FILTER = 'FOLDER_FILTER'
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
const TAG_FILTER = 'TAG_FILTER'
const OSX = global.process.platform === 'darwin'
class HomePage extends React.Component {
componentDidMount () {
// React自体のKey入力はfocusされていないElementからは動かないため、
// `window`に直接かける
this.keyHandler = e => this.handleKeyDown(e)
window.addEventListener('keydown', this.keyHandler)
}
componentWillUnmount () {
window.removeEventListener('keydown', this.keyHandler)
}
handleKeyDown (e) {
if (isModalOpen()) {
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
remote.getCurrentWebContents().send('modal-confirm')
}
if (e.keyCode === 27) closeModal()
return
}
let { status, dispatch } = this.props
let { top, list } = this.refs
let listElement = ReactDOM.findDOMNode(list)
if (status.isTutorialOpen) {
dispatch(toggleTutorial())
e.preventDefault()
return
}
if (e.keyCode === 13 && top.isInputFocused()) {
listElement.focus()
return
}
if (e.keyCode === 27 && top.isInputFocused()) {
if (status.search.length > 0) top.escape()
else listElement.focus()
return
}
// Search inputがfocusされていたら大体のキー入力は無視される。
if (e.keyCode === 27) {
if (document.activeElement !== listElement) {
listElement.focus()
} else {
top.focusInput()
}
return
}
}
render () {
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
return (
<div className='HomePage'>
<ArticleNavigator
ref='nav'
dispatch={dispatch}
status={status}
user={user}
folders={folders}
allArticles={allArticles}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleTopBar
ref='top'
dispatch={dispatch}
status={status}
folders={folders}
/>
<ArticleList
ref='list'
dispatch={dispatch}
folders={folders}
articles={articles}
modified={modified}
activeArticle={activeArticle}
/>
<ArticleDetail
ref='detail'
dispatch={dispatch}
status={status}
tags={tags}
user={user}
folders={folders}
modified={modified}
activeArticle={activeArticle}
/>
</div>
)
}
}
// Ignore invalid key
function ignoreInvalidKey (key) {
return key.length > 0 && !key.match(/^\/\/$/) && !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 { user, folders, status } = state
let _articles = state.articles
let articles = _articles != null ? _articles.data : []
let modified = _articles != null ? _articles.modified : []
articles.sort((a, b) => {
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
if (match === 0) match = b.title.localeCompare(a.title)
if (match === 0) match = b.key.localeCompare(a.key)
return match
})
let allArticles = articles.slice()
let tags = _.uniq(allArticles.reduce((sum, article) => {
if (!_.isArray(article.tags)) return sum
return sum.concat(article.tags)
}, []))
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
// Filter articles
let filters = status.search.split(' ')
.map(key => key.trim())
.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)
}
}
// Grab active article
let activeArticle = _.findWhere(articles, {key: status.articleKey})
if (activeArticle == null) activeArticle = articles[0]
return {
user,
folders,
status,
articles,
allArticles,
modified,
activeArticle,
tags
}
}
HomePage.propTypes = {
status: PropTypes.shape(),
user: PropTypes.shape({
name: PropTypes.string
}),
articles: PropTypes.array,
allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(),
dispatch: PropTypes.func,
folders: PropTypes.array,
tags: PropTypes.array
}
export default connect(remap)(HomePage)

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

@@ -0,0 +1,230 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './Main.styl'
import { connect } from 'react-redux'
import SideNav from './SideNav'
import TopBar from './TopBar'
import NoteList from './NoteList'
import Detail from './Detail'
import dataApi from 'browser/main/lib/dataApi'
import _ 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) {
super(props)
let { config } = props
this.state = {
isRightSliderFocused: false,
listWidth: config.listWidth,
navWidth: config.listWidth,
isLeftSliderFocused: false
}
}
getChildContext () {
let { status, config } = this.props
return {
status,
config
}
}
componentDidMount () {
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()
.then((data) => {
dispatch({
type: 'INIT_ALL',
storages: data.storages,
notes: data.notes
})
if (data.storages.length < 1) {
modal.open(InitModal)
}
})
window.addEventListener('focus', focused)
}
componentWillUnmount () {
window.removeEventListener('focus', focused)
}
handleLeftSlideMouseDown (e) {
e.preventDefault()
this.setState({
isLeftSliderFocused: true
})
}
handleRightSlideMouseDown (e) {
e.preventDefault()
this.setState({
isRightSliderFocused: true
})
}
handleMouseUp (e) {
if (this.state.isRightSliderFocused) {
this.setState({
isRightSliderFocused: false
}, () => {
let { dispatch } = this.props
let newListWidth = this.state.listWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({listWidth: newListWidth})
dispatch({
type: 'SET_LIST_WIDTH',
listWidth: newListWidth
})
})
}
if (this.state.isLeftSliderFocused) {
this.setState({
isLeftSliderFocused: false
}, () => {
let { dispatch } = this.props
let navWidth = this.state.navWidth
// TODO: ConfigManager should dispatch itself.
ConfigManager.set({listWidth: navWidth})
dispatch({
type: 'SET_NAV_WIDTH',
listWidth: navWidth
})
})
}
}
handleMouseMove (e) {
if (this.state.isRightSliderFocused) {
let offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
} else if (newListWidth > 600) {
newListWidth = 600
}
this.setState({
listWidth: newListWidth
})
}
if (this.state.isLeftSliderFocused) {
let navWidth = e.pageX
if (navWidth < 80) {
navWidth = 80
} else if (navWidth > 600) {
navWidth = 600
}
this.setState({
navWidth: navWidth
})
}
}
render () {
let { config } = this.props
return (
<div
className='Main'
styleName='root'
onMouseMove={(e) => this.handleMouseMove(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'location'
])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.navWidth - 1}}
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',
'config',
'data',
'params',
'location'
])}
/>
<NoteList style={{width: this.state.listWidth}}
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'params',
'location'
])}
/>
<div styleName={this.state.isRightSliderFocused ? 'slider--active' : 'slider'}
style={{left: this.state.listWidth}}
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
draggable='false'
>
<div styleName='slider-hitbox' />
</div>
<Detail
style={{left: this.state.listWidth + 1}}
{..._.pick(this.props, [
'dispatch',
'data',
'config',
'params',
'location'
])}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}
/>
</div>
</div>
)
}
}
Main.childContextTypes = {
status: PropTypes.shape({
updateReady: PropTypes.bool.isRequired
}).isRequired,
config: PropTypes.shape({}).isRequired
}
Main.propTypes = {
dispatch: PropTypes.func,
data: PropTypes.shape({}).isRequired
}
export default connect((x) => x)(CSSModules(Main, styles))

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

@@ -0,0 +1,41 @@
.root
absolute top left bottom right
.body
absolute right top bottom
left $sideNav-width
.body--expanded
@extend .body
left $sideNav--folded-width
.slider
absolute top bottom
width 1px
background-color $ui-borderColor
border-width 0
border-style solid
border-color $ui-borderColor
.slider--active
@extend .slider
background-color $ui-button--active-backgroundColor
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize
body[data-theme="dark"]
.root
absolute top left bottom right
.slider
background-color $ui-dark-borderColor
border-color $ui-dark-borderColor
.slider--active
background-color $ui-button--active-backgroundColor

View File

@@ -1,36 +0,0 @@
const electron = require('electron')
const ipc = electron.ipcRenderer
import React, { PropTypes } from 'react'
import HomePage from './HomePage'
export default class MainContainer extends React.Component {
constructor (props) {
super(props)
this.state = {updateAvailable: false}
}
componentDidMount () {
ipc.on('update-available', function (message) {
this.setState({updateAvailable: true})
}.bind(this))
}
updateApp () {
ipc.send('update-app', 'Deal with it.')
}
render () {
return (
<div className='Main'>
{this.state.updateAvailable ? (
<button onClick={this.updateApp} className='appUpdateButton'><i className='fa fa-cloud-download'/> Update available!</button>
) : null}
<HomePage/>
</div>
)
}
}
MainContainer.propTypes = {
children: PropTypes.element
}

View File

@@ -0,0 +1,225 @@
.root
absolute left bottom
border-top $ui-border
top $topBar-height - 1
.control
absolute top left right
user-select none
height 25px
font-size 10px
border-bottom $ui-border
line-height 25px
display flex
background-color $ui-backgroundColor
color $ui-inactive-text-color
.control-sortBy
flex 1
padding-left 5px
.control-sortBy-select
margin-left 5px
padding 0
border none
background-color transparent
font-size 10px
.control-button
width 25px
padding 0
background-color transparent
border none
color $ui-inactive-text-color
transition 0.15s
&:active, &:active:hover
color $ui-active-color
&:hover
color $ui-text-color
.control-button-tooltip
opacity 1
.control-button--active
@extend .control-button
color $ui-active-color
&:hover
color $ui-active-color
.control-button-tooltip
tooltip()
position absolute
top 20px
right 5px
padding 5px
opacity 0
white-space nowrap
border-radius 2px
z-index 1
.list
absolute left right bottom
top 24px
overflow auto
.item
position relative
border-bottom $ui-border
padding 2px 5px
user-select none
cursor pointer
transition background-color 0.15s
&:hover
background-color alpha($ui-active-color, 20%)
&:active
background-color $ui-active-color
color white
.item-title
.item-title-empty
.item-title-icon
.item-bottom-tagIcon
.item-bottom-tagList-empty
.item-bottom-time
color white
.item-bottom-tagList-item
background-color transparent
color white
.item--active
@extend .item
background-color $ui-active-color
color white
.item-title
.item-title-empty
.item-title-icon
.item-bottom-tagIcon
.item-bottom-tagList-empty
.item-bottom-time
color white
.item-bottom-tagList-item
background-color transparent
color white
&:hover
background-color $ui-active-color
.item-border
absolute top bottom left right
border-style solid
border-width 2px
border-color transparent
transition 0.15s
.item-title
height 24px
box-sizing border-box
line-height 24px
padding 0
overflow ellipsis
color $ui-text-color
.item-title-icon
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.item-title-empty
font-weight normal
color $ui-inactive-text-color
.item-bottom
margin-top 2px
height 20px
font-size 12px
line-height 20px
overflow ellipsis
display flex
.item-bottom-tagIcon
vertical-align middle
color $ui-button-color
height 20px
line-height 20px
.item-bottom-tagList
flex 1
overflow ellipsis
line-height 20px
.item-bottom-tagList-item
margin 0 4px
padding 0 4px
height 20px
box-sizing border-box
border-radius 3px
vertical-align middle
border-style solid
border-color $ui-button--focus-borderColor
border-width 0 0 0 3px
background-color $ui-backgroundColor
color $ui-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
font-size 10px
margin-left 5px
.item-bottom-time
color $ui-inactive-text-color
margin-left 5px
font-size 10px
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-backgroundColor
.item
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-active-color, 20%)
.item--active
@extend .item
border-color $ui-dark-borderColor
.item-title
color white
.item-bottom-tagList-item
background-color transparent
color white
.item-bottom-tagList-empty
color white
&:hover
background-color $ui-active-color
.item-title
color $ui-dark-text-color
.item-title-icon
color $ui-darkinactive-text-color
.item-title-empty
color $ui-dark-inactive-text-color
.item-bottom-tagIcon
color $ui-dark-button-color
.item-bottom-tagList-item
border-color $ui-dark-button--focus-borderColor
background-color $ui-dark-button--hover-backgroundColor
color $ui-dark-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle
.control
background-color $ui-dark-backgroundColor
border-color $ui-dark-borderColor
.control-sortBy-select
color $ui-dark-text-color
.control-button
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color

View File

@@ -0,0 +1,434 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import dataApi from 'browser/main/lib/dataApi'
import ConfigManager from 'browser/main/lib/ConfigManager'
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) {
super(props)
this.selectNextNoteHandler = () => {
console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
this.selectPriorNote()
}
this.focusHandler = () => {
this.refs.list.focus()
}
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)
}
componentWillReceiveProps (nextProps) {
if (nextProps.location.pathname !== this.props.location.pathname) {
this.resetScroll()
}
}
resetScroll () {
this.refs.list.scrollTop = 0
}
componentWillUnmount () {
clearInterval(this.refreshTimer)
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
ee.off('list:focus', this.focusHandler)
}
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].storage + '-' + this.notes[0].key
}
})
return
}
// Auto scroll
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
let targetIndex = _.findIndex(this.notes, (note) => {
return note != null && note.storage + '-' + note.key === location.query.key
})
if (targetIndex > -1) {
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
}
let overflowAbove = list.scrollTop > item.offsetTop
if (overflowAbove) {
list.scrollTop = item.offsetTop
}
}
}
}
selectPriorNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.storage + '-' + note.key === location.query.key
})
if (targetIndex === 0) {
return
}
targetIndex--
if (targetIndex < 0) targetIndex = 0
router.push({
pathname: location.pathname,
query: {
key: this.notes[targetIndex].storage + '-' + this.notes[targetIndex].key
}
})
}
selectNextNote () {
if (this.notes == null || this.notes.length === 0) {
return
}
let { router } = this.context
let { location } = this.props
let targetIndex = _.findIndex(this.notes, (note) => {
return note.storage + '-' + note.key === location.query.key
})
if (targetIndex === this.notes.length - 1) {
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].storage + '-' + this.notes[targetIndex].key
}
})
ee.emit('list:moved')
}
handleNoteListKeyDown (e) {
if (e.metaKey || e.ctrlKey) return true
if (e.keyCode === 65 && !e.shiftKey) {
e.preventDefault()
ee.emit('top:new-note')
}
if (e.keyCode === 68) {
e.preventDefault()
ee.emit('detail:delete')
}
if (e.keyCode === 69) {
e.preventDefault()
ee.emit('detail:focus')
}
if (e.keyCode === 38) {
e.preventDefault()
this.selectPriorNote()
}
if (e.keyCode === 40) {
e.preventDefault()
this.selectNextNote()
}
}
getNotes () {
let { data, params, location } = this.props
if (location.pathname.match(/\/home/)) {
return data.noteMap.map((note) => note)
}
if (location.pathname.match(/\/starred/)) {
return data.starredSet.toJS()
.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
let storageKey = params.storageKey
let folderKey = params.folderKey
let storage = data.storageMap.get(storageKey)
if (storage == null) return []
let folder = _.find(storage.folders, {key: folderKey})
if (folder == null) {
let storageNoteSet = data.storageNoteMap
.get(storage.key)
if (storageNoteSet == null) storageNoteSet = []
return storageNoteSet
.map((uniqueKey) => data.noteMap.get(uniqueKey))
}
let folderNoteKeyList = data.folderNoteMap
.get(storage.key + '-' + folder.key)
return folderNoteKeyList != null
? folderNoteKeyList
.map((uniqueKey) => data.noteMap.get(uniqueKey))
: []
}
handleNoteClick (e, uniqueKey) {
let { router } = this.context
let { location } = this.props
router.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
}
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
})
}
render () {
let { location, notes, config } = this.props
let sortFunc = config.sortBy === 'CREATED_AT'
? sortByCreatedAt
: config.sortBy === 'ALPHABETICAL'
? sortByAlphabetical
: sortByUpdatedAt
this.notes = notes = this.getNotes()
.sort(sortFunc)
let noteList = notes
.map((note) => {
if (note == null) return null
let tagElements = _.isArray(note.tags)
? note.tags.map((tag) => {
return (
<span styleName='item-bottom-tagList-item'
key={tag}>
{tag}
</span>
)
})
: []
let isActive = location.query.key === note.storage + '-' + note.key
return (
<div styleName={isActive
? 'item--active'
: 'item'
}
key={note.storage + '-' + note.key}
onClick={(e) => this.handleNoteClick(e, note.storage + '-' + note.key)}
onContextMenu={(e) => this.handleNoteContextMenu(e, note.storage + '-' + note.key)}
>
<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>
{config.listStyle === 'DEFAULT' &&
<div styleName='item-bottom'>
<i styleName='item-bottom-tagIcon'
className='fa fa-tags fa-fw'
/>
<div styleName='item-bottom-tagList'>
{tagElements.length > 0
? tagElements
: <span styleName='item-bottom-tagList-empty'>Not tagged yet</span>
}
</div>
<div styleName='item-bottom-time'>
{moment(config.sortBy === 'CREATED_AT' ? note.createdAt : note.updatedAt).fromNow()}
</div>
</div>
}
</div>
)
})
return (
<div className='NoteList'
styleName='root'
style={this.props.style}
>
<div styleName='control'>
<div styleName='control-sortBy'>
Sort by
<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-list'/>
<span styleName='control-button-tooltip'>
Default Size
</span>
</button>
<button styleName={config.listStyle === 'SMALL'
? 'control-button--active'
: 'control-button'
}
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
>
<i className='fa fa-list'/>
<span styleName='control-button-tooltip'>
Small Size
</span>
</button>
</div>
<div styleName='list'
ref='list'
tabIndex='-1'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
>
{noteList}
</div>
</div>
)
}
}
NoteList.contextTypes = {
router: PropTypes.shape([])
}
NoteList.propTypes = {
dispatch: PropTypes.func,
repositories: PropTypes.array,
style: PropTypes.shape({
width: PropTypes.number
})
}
export default CSSModules(NoteList, styles)

View File

@@ -0,0 +1,153 @@
.root
absolute top left bottom
width $sideNav-width
border-right $ui-border
background-color $ui-backgroundColor
user-select none
color $ui-text-color
.top
height $topBar-height
border-bottom $ui-border
.top-menu
navButtonColor()
height $topBar-height - 1
padding 0 10px
font-size 14px
width 100%
text-align left
.top-menu-label
margin-left 5px
overflow ellipsis
.menu
margin 0
.menu-button
navButtonColor()
height 32px
padding 0 10px
font-size 14px
width 100%
text-align left
overflow ellipsis
.menu-button--active
@extend .menu-button
background-color $ui-button--active-backgroundColor
color $ui-button--active-color
&:hover
background-color $ui-button--active-backgroundColor
.menu-button-label
margin-left 5px
.storageList
absolute left right
bottom 32px
top 120px
overflow-y auto
.storageList-empty
padding 0 10px
margin-top 15px
line-height 24px
color $ui-inactive-text-color
.navToggle
navButtonColor()
display block
position absolute
right 5px
bottom 5px
border-radius 16.5px
height 34px
width 34px
line-height 32px
padding 0
.root--folded
@extend .root
width 44px
.storageList-empty
white-space nowrap
transform rotate(90deg)
.top-menu
width 44px - 1
text-align center
&:hover .top-menu-label
transition opacity 0.15s
opacity 1
.top-menu-label
position fixed
display inline-block
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 30px
border-top-right-radius 2px
border-bottom-right-radius 2px
pointer-events none
font-size 12px
.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"]
.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()
.menu-button
navDarkButtonColor()
.menu-button--active
@extend .menu-button
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-button--active-color
&:hover
background-color $ui-dark-button--active-backgroundColor
.storageList-empty
color $ui-dark-inactive-text-color
.navToggle
navDarkButtonColor()

View File

@@ -0,0 +1,224 @@
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'
const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote
class StorageItem extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: true
}
}
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)
}
handleFolderButtonClick (folderKey) {
return (e) => {
let { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
}
}
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 <button styleName={isActive
? 'folderList-item--active'
: 'folderList-item'
}
key={folder.key}
onClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
onContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
>
<span styleName='folderList-item-name'
style={{borderColor: folder.color}}
>
{isFolded ? folder.name.substring(0, 1) : folder.name}
</span>
{!isFolded &&
<span styleName='folderList-item-noteCount'>{noteCount}</span>
}
{isFolded &&
<span styleName='folderList-item-tooltip'>
{folder.name}
</span>
}
</button>
})
let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$'))
return (
<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)}
>
<i className={this.state.isOpen
? 'fa fa-caret-down'
: 'fa fa-caret-right'
}
/>
</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>
}
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
{folderList}
</div>
}
</div>
)
}
}
StorageItem.propTypes = {
isFolded: PropTypes.bool
}
export default CSSModules(StorageItem, styles)

View File

@@ -0,0 +1,182 @@
.root
width 100%
user-select none
.header
position relative
height 26px
width 100%
transition 0.15s
&:hover
background-color $ui-button--hover-backgroundColor
&:active
.header-toggleButton
.header-addFolderButton
color white
&:active
color $ui-active-color
.header--active
@extend .header
.header-info
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-toggleButton
.header-addFolderButton
color white
&:active
&:hover
&:hover:active
color white
.header-toggleButton
position absolute
left 0
width 25px
height 26px
padding 0
border none
color $ui-inactive-text-color
background-color transparent
&:hover
color $ui-text-color
&:active
color $ui-active-color
.header-info
display block
width 100%
height 26px
padding-left 25px
padding-right 10px
line-height 26px
cursor pointer
font-size 14px
border none
overflow ellipsis
text-align left
background-color transparent
color $ui-inactive-text-color
&:active
color $ui-button--active-color
background-color $ui-button--active-backgroundColor
.header-info-path
font-size 10px
margin 0 5px
.header-addFolderButton
position absolute
right 0
width 25px
height 26px
padding 0
border none
color $ui-inactive-text-color
background-color transparent
&:hover
color $ui-text-color
&:active
color $ui-active-color
.folderList-item
display flex
width 100%
height 26px
background-color transparent
color $ui-inactive-text-color
padding 0
margin-bottom 2px
text-align left
border none
overflow ellipsis
font-size 14px
&:first-child
margin-top 0
&: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
flex 1
padding 0 10px
height 26px
line-height 26px
border-width 0 0 0 6px
border-style solid
border-color transparent
.folderList-item-noteCount
float right
line-height 26px
padding-right 5px
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
.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
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
.folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip
opacity 1
.folderList-item-name
padding-left 14px
body[data-theme="dark"]
.header-toggleButton
.header-addFolderButton
color $ui-dark-inactive-text-color
&:hover
color $ui-dark-text-color
&:active
color $ui-dark-active-color
.header--active
.header-toggleButton
.header-addFolderButton
color white
&:active
color white

View File

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

View File

@@ -0,0 +1,56 @@
.root
absolute bottom left right
height $statusBar-height
background-color $ui-backgroundColor
border-top $ui-border
display flex
.blank
flex 1
.help
navButtonColor()
height 24px
width 24px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
.zoom
navButtonColor()
height 24px
border-width 0 1px
border-style solid
border-color $ui-borderColor
.update
navButtonColor()
height 24px
border-width 0 0 0 1px
border-style solid
border-color $ui-borderColor
&:active .update-icon
color white
.update-icon
color $brand-color
body[data-theme="dark"]
.root
background-color $ui-dark-backgroundColor
border-color $ui-dark-borderColor
.zoom
border-color $ui-dark-borderColor
.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

@@ -0,0 +1,89 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
const electron = require('electron')
const { remote, ipcRenderer } = electron
const { Menu, MenuItem, dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3]
class StatusBar extends React.Component {
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')
}
}
handleZoomButtonClick (e) {
let menu = new Menu()
zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({
label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom)
}))
})
menu.popup(remote.getCurrentWindow())
}
handleZoomMenuItemClick (zoomFactor) {
let { dispatch } = this.props
ZoomManager.setZoom(zoomFactor)
dispatch({
type: 'SET_ZOOM',
zoom: zoomFactor
})
}
render () {
let { config, status } = this.context
return (
<div className='StatusBar'
styleName='root'
>
<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
}
{/*<button styleName='help'>
<i className='fa fa-info-circle' />
</button>*/}
<button styleName='zoom'
onClick={(e) => this.handleZoomButtonClick(e)}
>
<i className='fa fa-search-plus' />&nbsp;
{Math.floor(config.zoom * 100)}%
</button>
</div>
)
}
}
StatusBar.contextTypes = {
status: PropTypes.shape({
updateReady: PropTypes.bool.isRequired
}).isRequired,
config: PropTypes.shape({}).isRequired
}
StatusBar.propTypes = {
config: PropTypes.shape({
zoom: PropTypes.number
})
}
export default CSSModules(StatusBar, styles)

View File

@@ -0,0 +1,173 @@
.root
position relative
background-color $ui-backgroundColor
height $topBar-height - 1
.root--expanded
@extend .root
$control-height = 34px
.control
position absolute
top 8px
left 8px
right 8px
height $control-height
border $ui-border
border-radius 20px
overflow hidden
display flex
.control-search
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
.control-search-input
display block
absolute top bottom right
left 30px
input
width 100%
height 100%
outline none
border none
.control-search-optionList
position fixed
z-index 200
width 275px
height 175px
overflow-y auto
background-color $modal-background
border-radius 2px
box-shadow 2px 2px 10px gray
.control-search-optionList-item
height 50px
border-bottom $ui-border
transition background-color 0.15s
padding 5px
cursor pointer
overflow ellipsis
&:hover
background-color alpha($ui-active-color, 10%)
.control-search-optionList-item-folder
border-left 4px solid transparent
padding 2px 5px
color $ui-text-color
overflow ellipsis
font-size 12px
height 16px
margin-bottom 4px
.control-search-optionList-item-folder-surfix
font-size 10px
margin-left 5px
color $ui-inactive-text-color
.control-search-optionList-item-type
font-size 12px
color $ui-inactive-text-color
padding-right 3px
.control-search-optionList-empty
height 150px
color $ui-inactive-text-color
line-height 150px
text-align center
.control-contextButton
display block
width 20px
height $control-height - 2
navButtonColor()
border-left $ui-border
font-size 14px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .control-newPostButton-tooltip
opacity 1
.control-newPostButton
display block
width 36px
height $control-height - 2
navButtonColor()
border-left $ui-border
font-size 14px
line-height 28px
padding 0
&:active
border-color $ui-button--active-backgroundColor
&:hover .control-newPostButton-tooltip
opacity 1
.control-newPostButton-tooltip
tooltip()
position fixed
pointer-events none
top 45px
left 385px
z-index 10
padding 5px
line-height normal
opacity 0
transition 0.1s
body[data-theme="dark"]
.root, .root--expanded
background-color $ui-dark-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
.control-search-input
input
background-color $dark-background-color
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 lighten($ui-dark-button--hover-backgroundColor, 15%)
.control-search-optionList-item-folder
color $ui-dark-text-color
.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
color $ui-inactive-text-color
.control-contextButton,
.control-newPostButton
colorDarkDefaultButton()
border-color $ui-dark-borderColor
&:active
border-color $ui-dark-button--active-backgroundColor
.control-newPostButton-tooltip
darkTooltip()

View File

@@ -0,0 +1,383 @@
import React, { PropTypes } from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TopBar.styl'
import _ from 'lodash'
import modal from 'browser/main/lib/modal'
import NewNoteModal from 'browser/main/modals/NewNoteModal'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import ConfigManager from 'browser/main/lib/ConfigManager'
import dataApi from 'browser/main/lib/dataApi'
const OSX = window.process.platform === 'darwin'
const { remote } = require('electron')
const { Menu, MenuItem } = remote
class TopBar extends React.Component {
constructor (props) {
super(props)
this.state = {
search: '',
searchOptions: [],
searchPopupOpen: false
}
this.newNoteHandler = () => {
this.handleNewPostButtonClick()
}
}
componentDidMount () {
ee.on('top:new-note', this.newNoteHandler)
}
componentWillUnmount () {
ee.off('top:new-note', this.newNoteHandler)
}
handleNewPostButtonClick (e) {
let { 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,
folder: folder.key,
dispatch,
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) 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')
return {
storage,
folder
}
}
handleSearchChange (e) {
this.setState({
search: this.refs.searchInput.value
})
}
getOptions () {
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]
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 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(regExp)
} else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(regExp)
}
return false
})
}
})
return notes
}
handleOptionClick (uniqueKey) {
return (e) => {
this.setState({
searchPopupOpen: false
}, () => {
let { location } = this.props
hashHistory.push({
pathname: location.pathname,
query: {
key: uniqueKey
}
})
})
}
}
handleSearchFocus (e) {
this.setState({
searchPopupOpen: true
})
}
handleSearchBlur (e) {
e.stopPropagation()
let el = e.relatedTarget
let isStillFocused = false
while (el != null) {
if (el === this.refs.search) {
isStillFocused = true
break
}
el = el.parentNode
}
if (!isStillFocused) {
this.setState({
searchPopupOpen: false
})
}
}
handleContextButtonClick (e) {
let { config } = this.props
let menu = new Menu()
menu.append(new MenuItem({
label: 'Create Markdown Note',
click: (e) => this.createNote('MARKDOWN_NOTE')
}))
menu.append(new MenuItem({
label: 'Create Snippet Note',
click: (e) => this.createNote('SNIPPET_NOTE')
}))
menu.append(new MenuItem({
type: 'separator'
}))
menu.append(new MenuItem({
label: 'Change Default Note',
submenu: [
{
type: 'radio',
label: 'Markdown Note',
checked: config.ui.defaultNote === 'MARKDOWN_NOTE',
click: (e) => this.setDefaultNote('MARKDOWN_NOTE')
},
{
type: 'radio',
label: 'Snippet Note',
checked: config.ui.defaultNote === 'SNIPPET_NOTE',
click: (e) => this.setDefaultNote('SNIPPET_NOTE')
},
{
type: 'radio',
label: 'Always Ask',
checked: config.ui.defaultNote === 'ALWAYS_ASK',
click: (e) => this.setDefaultNote('ALWAYS_ASK')
}
]
}))
menu.popup(remote.getCurrentWindow())
}
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()
})
}
render () {
let { config, style, data } = this.props
let searchOptionList = this.getOptions()
.map((note) => {
let storage = data.storageMap.get(note.storage)
let folder = _.find(storage.folders, {key: note.folder})
return <div styleName='control-search-optionList-item'
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}}>
{folder.name}
<span styleName='control-search-optionList-item-folder-surfix'>in {storage.name}</span>
</div>
{note.type === 'SNIPPET_NOTE'
? <i styleName='control-search-optionList-item-type' className='fa fa-code'/>
: <i styleName='control-search-optionList-item-type' className='fa fa-file-text-o'/>
}&nbsp;
{note.title}
</div>
})
return (
<div className='TopBar'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style}
>
<div styleName='control'>
<div styleName='control-search'>
<i styleName='control-search-icon' className='fa fa-search fa-fw'/>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
tabIndex='-1'
ref='search'
>
<input
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
placeholder='Search'
type='text'
/>
{this.state.searchPopupOpen &&
<div styleName='control-search-optionList'>
{searchOptionList.length > 0
? searchOptionList
: <div styleName='control-search-optionList-empty'>Empty List</div>
}
</div>
}
</div>
{this.state.search > 0 &&
<button styleName='left-search-clearButton'
onClick={(e) => this.handleSearchClearButton(e)}
>
<i className='fa fa-times'/>
</button>
}
</div>
<button styleName='control-newPostButton'
onClick={(e) => this.handleNewPostButtonClick(e)}>
<i className='fa fa-plus'/>
<span styleName='control-newPostButton-tooltip'>
New Note {OSX ? '⌘' : '^'} + n
</span>
</button>
<button styleName='control-contextButton'
onClick={(e) => this.handleContextButtonClick(e)}
>
<i className='fa fa-caret-down'/>
</button>
</div>
</div>
)
}
}
TopBar.contextTypes = {
router: PropTypes.shape({
push: PropTypes.func
})
}
TopBar.propTypes = {
dispatch: PropTypes.func,
config: PropTypes.shape({
isSideNavFolded: PropTypes.bool
})
}
export default CSSModules(TopBar, styles)

View File

@@ -1,178 +0,0 @@
// Action types
export const USER_UPDATE = 'USER_UPDATE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Article status
export const NEW = 'NEW'
export function updateUser (input) {
return {
type: USER_UPDATE,
data: input
}
}
// DB
export function cacheArticle (key, article) {
return {
type: ARTICLE_CACHE,
data: { key, article }
}
}
export function uncacheArticle (key) {
return {
type: ARTICLE_UNCACHE,
data: { key }
}
}
export function uncacheAllArticles () {
return {
type: ARTICLE_UNCACHE_ALL
}
}
export function saveArticle (key, article, forceSwitch) {
return {
type: ARTICLE_SAVE,
data: { key, article, forceSwitch }
}
}
export function saveAllArticles () {
return {
type: ARTICLE_SAVE_ALL
}
}
export function updateArticle (article) {
return {
type: ARTICLE_UPDATE,
data: { article }
}
}
export function destroyArticle (key) {
return {
type: ARTICLE_DESTROY,
data: { key }
}
}
export function createFolder (folder) {
return {
type: FOLDER_CREATE,
data: { folder }
}
}
export function updateFolder (folder) {
return {
type: FOLDER_UPDATE,
data: { folder }
}
}
export function destroyFolder (key) {
return {
type: FOLDER_DESTROY,
data: { key }
}
}
export function replaceFolder (a, b) {
return {
type: FOLDER_REPLACE,
data: {
a,
b
}
}
}
export function switchFolder (folderName) {
return {
type: SWITCH_FOLDER,
data: folderName
}
}
export function switchArticle (articleKey) {
return {
type: SWITCH_ARTICLE,
data: {
key: articleKey
}
}
}
export function setSearchFilter (search) {
return {
type: SET_SEARCH_FILTER,
data: search
}
}
export function setTagFilter (tag) {
return {
type: SET_TAG_FILTER,
data: tag
}
}
export function clearSearch () {
return {
type: CLEAR_SEARCH
}
}
export function toggleTutorial () {
return {
type: TOGGLE_TUTORIAL
}
}
export default {
updateUser,
updateArticle,
destroyArticle,
cacheArticle,
uncacheArticle,
uncacheAllArticles,
saveArticle,
saveAllArticles,
createFolder,
updateFolder,
destroyFolder,
replaceFolder,
switchFolder,
switchArticle,
setSearchFilter,
setTagFilter,
clearSearch,
toggleTutorial
}

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

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

@@ -1,67 +1,89 @@
import { Provider } from 'react-redux'
import MainPage from './MainPage'
import Main from './Main'
import store from './store'
import React from 'react'
import ReactDOM from 'react-dom'
require('../styles/main/index.styl')
import { openModal } from 'browser/lib/modal'
import Tutorial from './modal/Tutorial'
import activityRecord from 'browser/lib/activityRecord'
require('!!style!css!stylus?sourceMap!./global.styl')
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()
}
})
}
const { remote, ipcRenderer } = electron
const { dialog } = remote
activityRecord.init()
window.addEventListener('online', function () {
ipc.send('check-update', 'check-update')
document.addEventListener('drop', function (e) {
e.preventDefault()
e.stopPropagation()
})
function notify (title, options) {
if (process.platform === 'win32') {
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
options.silent = false
}
console.log(options)
return new window.Notification(title, options)
}
ipc.on('notify', function (e, payload) {
notify(payload.title, {
body: payload.body
})
})
ipc.on('copy-finder', function () {
activityRecord.emit('FINDER_COPY')
})
ipc.on('open-finder', function () {
activityRecord.emit('FINDER_OPEN')
document.addEventListener('dragover', function (e) {
e.preventDefault()
e.stopPropagation()
})
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((
<div>
<Provider store={store}>
<MainPage/>
</Provider>
</div>
<Provider store={store}>
<Router history={history}>
<Route path='/' component={Main}>
<IndexRedirect to='/home' />
<Route path='home' />
<Route path='starred' />
<Route path='storages'>
<IndexRedirect to='/home' />
<Route path=':storageKey'>
<IndexRoute />
<Route path='folders/:folderKey' />
</Route>
</Route>
</Route>
</Router>
</Provider>
), el, function () {
let loadingCover = document.getElementById('loadingCover')
loadingCover.parentNode.removeChild(loadingCover)
let status = JSON.parse(localStorage.getItem('status'))
if (status == null) status = {}
if (!status.introWatched) {
openModal(Tutorial)
status.introWatched = true
localStorage.setItem('status', JSON.stringify(status))
}
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

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

View File

@@ -0,0 +1,130 @@
import _ from 'lodash'
const OSX = global.process.platform === 'darwin'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
let isInitialized = false
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
listWidth: 250,
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,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
},
editor: {
theme: 'default',
fontSize: '14',
fontFamily: 'Monaco, Consolas',
indentType: 'space',
indentSize: '2',
switchPreview: 'BLUR' // Available value: RIGHTCLICK, BLUR
},
preview: {
fontSize: '14',
fontFamily: 'Lato',
codeBlockTheme: 'xcode',
lineNumber: true
}
}
function validate (config) {
if (!_.isObject(config)) return false
if (!_.isNumber(config.zoom) || config.zoom < 0) return false
if (!_.isBoolean(config.isSideNavFolded)) return false
if (!_.isNumber(config.listWidth) || config.listWidth <= 0) return false
return true
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
function get () {
let config = window.localStorage.getItem('config')
try {
config = Object.assign({}, 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 = 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({}, DEFAULT_CONFIG, currentConfig, updates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
if (newConfig.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)
}
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,
set,
validate
}

View File

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

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

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