Compare commits

...

221 Commits

Author SHA1 Message Date
azivner
c8b9c7d936 release 0.12.0 2018-04-14 08:28:50 -04:00
azivner
d57057ba28 fix note ordering sync 2018-04-14 08:23:06 -04:00
azivner
66cee8daa4 restructuring CSS grid/flex which fixes jumpy scrolling in tree 2018-04-13 19:58:33 -04:00
azivner
afd7df0942 fix collapse tree button 2018-04-13 19:22:12 -04:00
azivner
bd6ae33d32 fancytree upgrade to 2.28.1 2018-04-12 20:42:12 -04:00
azivner
70660a0d68 Merge branch 'stable' 2018-04-12 20:04:01 -04:00
azivner
cdad18551a upgrade CKEditor to 1.0 beta.2, fixes #93 2018-04-12 20:03:23 -04:00
azivner
592c51d1a5 fix note reordering sync again 2018-04-12 18:31:29 -04:00
azivner
6a57b8a7e7 fix ordering sync 2018-04-12 18:13:48 -04:00
azivner
7a94e21c54 tabindex 2 for text and code editor so that tabbing from title leads to editor focus 2018-04-11 22:44:33 -04:00
azivner
5b43f321e2 release 0.11.1 2018-04-11 00:10:33 -04:00
azivner
a4eafb934f non null note title and content in the DB schema, allow saving non-valid JSON notes, children overview style changes 2018-04-11 00:10:11 -04:00
azivner
7b59a665dd hideChildrenOverview label which can disable children overview for specific notes 2018-04-10 23:15:41 -04:00
azivner
3d15450ffc children overview styling 2018-04-10 21:08:00 -04:00
azivner
b0c6d52461 can't rollback transaction multiple times 2018-04-10 20:28:02 -04:00
azivner
2dc16dd29f release 0.11.0-beta 2018-04-09 22:38:37 -04:00
azivner
d8924c536b Merge branch 'master' into stable 2018-04-09 22:30:50 -04:00
azivner
3ebbf2cc46 fix generating build.js 2018-04-09 22:30:11 -04:00
azivner
f4079604c9 basic implementation of children overview, closes #80 2018-04-08 22:38:52 -04:00
azivner
1f96a6beab export & import work correctly with clones 2018-04-08 13:14:30 -04:00
azivner
b277a250e5 protected notes are not in autocomplete when not in protected session, fixes #46 2018-04-08 12:27:10 -04:00
azivner
5b0e1a644d codemirror now doesn't hijack alt-left/right, fixes #86 2018-04-08 12:17:42 -04:00
azivner
6bb3cfa9a3 note revisions for code is now properly formatted, fixes #97 2018-04-08 12:13:52 -04:00
azivner
9720868f5a added type and mime to note revisions 2018-04-08 11:57:14 -04:00
azivner
8d8ee2a87a small sync refactorings 2018-04-08 10:09:33 -04:00
azivner
542e82ee5d upgraded uncompressed jquery 2018-04-08 09:40:28 -04:00
azivner
0104b19502 naming standards 2018-04-08 09:25:35 -04:00
azivner
120888b53e fix JSON saving bug 2018-04-08 08:31:19 -04:00
azivner
d2e2caed62 refactoring of note saving code & API 2018-04-08 08:21:49 -04:00
azivner
63066802a8 fix showMessage, showError
(cherry picked from commit 6128bb4)
2018-04-08 07:49:21 -04:00
azivner
6128bb4ff3 fix showMessage, showError 2018-04-08 07:48:47 -04:00
azivner
982796255d sync content check refactoring 2018-04-07 22:59:47 -04:00
azivner
36b15f474d sync cleanup 2018-04-07 22:32:46 -04:00
azivner
13f71f8967 bulk push sync 2018-04-07 22:25:28 -04:00
azivner
64336ffbee implemented bulk sync pull for increased performance 2018-04-07 21:53:42 -04:00
azivner
b09463d1b2 async logging of info messages 2018-04-07 21:30:01 -04:00
azivner
b5e6f46b9c release 0.10.2-beta 2018-04-07 16:07:25 -04:00
azivner
08af4a0465 fix code mirror loading 2018-04-07 15:56:46 -04:00
azivner
8c5df6321f fix windows sqlite binary for electron 2.0 2018-04-07 13:18:08 -04:00
azivner
d19f044961 fix bug 2018-04-07 13:14:01 -04:00
azivner
e378d9f645 label service refactoring + rename of doInTransaction to transactional 2018-04-07 13:03:16 -04:00
azivner
39dc0f71b4 fix execute note 2018-04-06 19:41:48 -04:00
azivner
0cef5c6b8c added showMessage/showError to script api as they are being used 2018-04-06 19:08:42 -04:00
azivner
9b5a44cef4 fix bugs 2018-04-06 18:49:37 -04:00
azivner
29769ed91d fix force note sync 2018-04-06 18:46:29 -04:00
azivner
867d794e17 release 0.10.1-beta 2018-04-06 00:15:04 -04:00
azivner
fdd8458336 fix sync branch route 2018-04-05 23:45:39 -04:00
azivner
a0bec22e96 fix non-200 logging 2018-04-05 23:35:49 -04:00
azivner
5aeb5cd214 jquery upgrade to 3.3.1 2018-04-05 23:18:15 -04:00
azivner
e827ddffb9 electron fixes 2018-04-05 23:17:19 -04:00
azivner
98f80998b9 fix electron build 2018-04-05 19:29:27 -04:00
azivner
69727d0b12 release 0.10.0-beta 2018-04-04 23:57:46 -04:00
azivner
84faf32b98 updated scripts 2018-04-04 23:55:19 -04:00
azivner
6ed6e27602 startup script running fix 2018-04-04 23:51:47 -04:00
azivner
fb54678fef getNoteWithLabel fix 2018-04-04 23:43:54 -04:00
azivner
2cdcb3af12 camel casing and fixes 2018-04-04 23:04:31 -04:00
azivner
cf7a336ac2 camel case for reddit labels and run values 2018-04-04 22:29:11 -04:00
azivner
abfc64af95 script to generate large documents, closes #55 2018-04-03 22:15:28 -04:00
azivner
42dd8d4754 smaller refactorings 2018-04-02 22:53:01 -04:00
azivner
a4e64350e9 fixed schema, initial setup 2018-04-02 22:33:54 -04:00
azivner
6f567e3e10 camelCase builtin labels 2018-04-02 21:56:55 -04:00
azivner
c6c76ba360 option names now follow camelCase 2018-04-02 21:47:46 -04:00
azivner
429d3f518e moved instanceName to index.ejs 2018-04-02 21:34:28 -04:00
azivner
26e4ad9bf9 separated DB initialization methods into sql_init 2018-04-02 21:25:20 -04:00
azivner
6ab0cea4e3 split out dateUtils on the backend 2018-04-02 20:46:46 -04:00
azivner
277368ab43 simplified new entity ID allocation 2018-04-02 20:30:00 -04:00
azivner
e2921a648d refactored backend to use new naming convention for modules 2018-04-01 21:27:46 -04:00
azivner
c765dbc5cf continuing in API review 2018-04-01 20:50:58 -04:00
azivner
a066c6fe2b changes in API format 2018-04-01 20:33:10 -04:00
azivner
311952d4dd renamed settings to options for consistency 2018-04-01 17:41:28 -04:00
azivner
96dab5d51e smaller refactorings continued 2018-04-01 17:38:24 -04:00
azivner
15d951b04e smaller refactorings continued 2018-04-01 12:45:35 -04:00
azivner
8ba830c04b smaller refactorings continued 2018-04-01 12:03:21 -04:00
azivner
acc82f39c4 smaller refactorings continued 2018-04-01 11:42:12 -04:00
azivner
fad0ec757b smaller refactorings (mostly entitization) 2018-04-01 11:05:09 -04:00
azivner
c9d73c6115 renamed outstanding attribute references to labels 2018-04-01 09:59:44 -04:00
azivner
ab2f28ceef added missing sync check hashes 2018-03-31 23:19:54 -04:00
azivner
87e415992c removed support for old option schema of opt_name and opt_value 2018-03-31 23:11:43 -04:00
azivner
12439d8761 refactoring of note deletion 2018-03-31 23:08:22 -04:00
azivner
4f200c73dc refactoring of note creation 2018-03-31 22:23:40 -04:00
azivner
5f7e74e15c refactoring of note update 2018-03-31 22:15:06 -04:00
azivner
e8a5d0ae16 converted note revision protection to repository/entities 2018-03-31 10:51:37 -04:00
azivner
088fb00ca9 repository is now stateless 2018-03-31 09:07:58 -04:00
azivner
05676f3459 removed dataKey where it's not necessary anymore (use of CLS instead) 2018-03-31 08:53:52 -04:00
azivner
5d203b2278 removed sourceId where it's not necessary (stored in CLS instead) 2018-03-30 19:41:54 -04:00
azivner
795d50f02e converted of web (non-api) routes, basic conversion completed 2018-03-30 19:31:22 -04:00
azivner
cfe0ae1eda converted file, script, search and sender routes 2018-03-30 17:29:13 -04:00
azivner
aa57a64c61 converted image and maintainance routes 2018-03-30 17:07:41 -04:00
azivner
e36a81e189 converted export/import notes 2018-03-30 15:34:07 -04:00
azivner
88c07a9e48 converted sync route 2018-03-30 14:27:41 -04:00
azivner
bfd9f292a6 Merge remote-tracking branch 'origin/stable' 2018-03-30 14:05:11 -04:00
azivner
9edee9340b converted settings, note revisions, password change and recent changes routes 2018-03-30 13:56:46 -04:00
azivner
8550ed72f2 converted cloning and label routes 2018-03-30 13:20:36 -04:00
azivner
efffc29649 initial work on new router model 2018-03-30 12:57:22 -04:00
azivner
0ec909fd7a added basic CLS support with re-entrant transactions 2018-03-28 23:41:22 -04:00
azivner
b10b0048f3 split out library loader 2018-03-27 22:42:46 -04:00
azivner
9bb188b519 fix unnecessary popups about leaving the page 2018-03-27 22:27:46 -04:00
azivner
7464835058 renamed "attachment" to "file" for consistency 2018-03-27 22:11:06 -04:00
azivner
913b6bb6f6 abstracted note detail components 2018-03-27 21:46:38 -04:00
azivner
000cf99546 split out render and search from note detail service 2018-03-27 21:36:01 -04:00
azivner
c918267750 separated attachments out of note detail 2018-03-27 00:27:38 -04:00
azivner
68921ee59b separated text and code handling out of note detail service 2018-03-27 00:22:02 -04:00
azivner
7e856283ee some refactorings of note detail service 2018-03-26 23:48:45 -04:00
azivner
9c1b8da573 split out keybindings out of tree service 2018-03-26 23:25:54 -04:00
azivner
cb39b9cca8 split out tree_builder 2018-03-26 23:18:50 -04:00
azivner
788ac43ad1 further refactorings, got rid of init.js 2018-03-26 22:29:14 -04:00
azivner
57d19f3302 refactored moving note in the tree 2018-03-26 22:11:45 -04:00
azivner
68bba623b6 split up autocomplete related functionality 2018-03-26 21:50:47 -04:00
azivner
35998058ce introduced NoteFull entity, fixes 2018-03-25 23:25:17 -04:00
azivner
cdf94181d2 got rid of instanceName in tree service 2018-03-25 22:56:23 -04:00
azivner
91ee90d827 cleanup in tree service 2018-03-25 22:37:02 -04:00
azivner
d3316cd09c reduced dependencies of utils 2018-03-25 21:29:35 -04:00
azivner
ac1b06967f decoupled protected session holder from presentation stuff and similar things 2018-03-25 21:16:57 -04:00
azivner
47eb1e3e02 refactored all mentions of "history" to "revision" 2018-03-25 20:52:38 -04:00
azivner
a69d8737ce fixed image upload and eslint 2018-03-25 20:18:08 -04:00
azivner
341f47f0f2 moved dialog entrypoints into bootstrap, fixes 2018-03-25 19:49:33 -04:00
azivner
19c605a9a8 labels have now alt-l shortcut to correspond with the renaming 2018-03-25 15:47:31 -04:00
azivner
54e4f54678 all access to notes and branches is now async so we can lazy load it in the future 2018-03-25 14:49:20 -04:00
azivner
297a2cd9da renamed service variables to conform to new naming scheme 2018-03-25 13:41:29 -04:00
azivner
d746d707b5 unblocking infinite cycle 2018-03-25 13:13:26 -04:00
azivner
299252b650 making WS connection asynchronous to not block module registration 2018-03-25 13:08:58 -04:00
azivner
fddd1c278f moved services into the service directory 2018-03-25 13:02:39 -04:00
azivner
f52d7e3c28 split tree_cache and entities from note_tree 2018-03-25 12:29:00 -04:00
azivner
a699210a29 using ES6 modules for whole frontend SPA app 2018-03-25 11:09:17 -04:00
azivner
b3c32a39e9 fix for moving the notes 2018-03-25 10:06:14 -04:00
azivner
df27533b66 fixes after refactorings 2018-03-25 00:20:55 -04:00
azivner
b96a1274c5 all JS functions are now inside old-school JS modules, preparation for ES6 modularization 2018-03-24 23:58:58 -04:00
azivner
001a5107dd ScriptApi separated from ScriptContext, preparation for modularisation 2018-03-24 23:45:36 -04:00
azivner
c8e456cdb1 refactoring utils into module 2018-03-24 23:37:55 -04:00
azivner
0f6b00e1c8 fixes after refactoring, base functionality works again 2018-03-24 23:00:12 -04:00
azivner
5ea060a054 renaming attributes to labels in readme 2018-03-24 22:17:31 -04:00
azivner
95bb2cf0bb renaming attributes to labels, fixes #79 2018-03-24 22:02:26 -04:00
azivner
4c472ce78b renaming note_tree to branch 2018-03-24 21:39:15 -04:00
azivner
511fb89af0 WIP refactoring of data structures in note tree 2018-03-24 20:41:27 -04:00
azivner
7e524c0cd1 removed some warnings in idea 2018-03-24 12:52:58 -04:00
azivner
e3e2dc9fff first POC of ES6 module 2018-03-24 11:18:46 -04:00
azivner
1612e9093d removed all onclick handlers from index template 2018-03-24 00:54:50 -04:00
azivner
f8649feea4 saved search can now be created from the search dialog 2018-03-23 23:08:29 -04:00
azivner
ac978c3fa7 upgrade to electron 2.0.0-beta.5, fixes #65 2018-03-23 21:01:49 -04:00
azivner
efcc804149 redesign search buttons 2018-03-13 20:02:00 -04:00
azivner
db514e8f41 display note type only for non-search notes 2018-03-13 19:47:34 -04:00
azivner
9c32f66329 context menu disables actions not applicable to search note 2018-03-13 19:31:07 -04:00
azivner
0fd5102a26 added consistency check for search note not containing children 2018-03-13 19:18:52 -04:00
azivner
840af15dae release 0.9.2 2018-03-13 08:22:25 -04:00
azivner
f1b0b3bcdb basic implementation of saved searches finished, closes #83 2018-03-12 23:27:21 -04:00
azivner
e5c0acbb43 note tree refactorings 2018-03-12 23:14:09 -04:00
azivner
834661c461 Merge remote-tracking branch 'origin/master' 2018-03-12 20:00:29 -04:00
azivner
5204ab5a7e refactoring of note tree 2018-03-12 20:00:19 -04:00
azivner
74862536a8 Merge branch 'stable' 2018-03-12 19:46:43 -04:00
zadam
a24f1f5b95 Merge pull request #90 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2018-03-11 18:52:32 -04:00
The Gitter Badger
0be76f746a Add Gitter badge 2018-03-11 22:46:54 +00:00
azivner
fad89ff63f added Theming to list of pages 2018-03-11 13:09:28 -04:00
azivner
b8ae791191 icon for saved seach 2018-03-11 10:53:10 -04:00
azivner
ce754cbd91 saving saved search, #73 2018-03-11 10:49:22 -04:00
azivner
0fdb6af98a Merge branch 'master' into stable 2018-03-10 20:33:58 -05:00
azivner
f6c7f6a0f2 added require() method for commonJS compliancy 2018-03-10 11:53:51 -05:00
azivner
354999f37a fix weight tracker script 2018-03-09 19:28:38 -05:00
azivner
348c622845 release 0.9.1-beta 2018-03-09 00:12:22 -05:00
azivner
44bcdedaba Updated all scripts to current versions working with current script API 2018-03-09 00:10:43 -05:00
azivner
755c0f3ce2 fix exclude from export 2018-03-09 00:10:02 -05:00
azivner
895bda41b5 return only startup bundles for executale notes 2018-03-08 23:36:08 -05:00
azivner
b2df622cb6 Merge branch 'stable' 2018-03-08 23:35:17 -05:00
azivner
9ba6e6d0f5 disable inclusion should work only on non-root notes 2018-03-08 23:35:08 -05:00
azivner
a5c9180533 release 0.9.0-beta 2018-03-08 20:18:37 -05:00
azivner
e86f1e0d05 changes to the HTML template to allow more complete styling 2018-03-07 23:58:34 -05:00
azivner
b6277049f3 added support for app_css attribute, which allows custom styling 2018-03-07 23:24:23 -05:00
azivner
c831221cc4 add "play" icon for "render" note types 2018-03-07 20:52:34 -05:00
azivner
577a168714 stop propagation of ctrl+enter from SQL console, fixes #73 2018-03-07 20:46:01 -05:00
azivner
b0bd27321a escape will close SQL console, closes #72 2018-03-07 20:33:41 -05:00
azivner
90c5348ca7 fix saving only image in a note, fixes #77 2018-03-07 20:19:53 -05:00
azivner
8e95b080da fixed render notes 2018-03-07 00:17:18 -05:00
azivner
766a567a32 changes in access to startNote and currentNote 2018-03-06 23:04:35 -05:00
azivner
6d0218cb36 execute note (ctrl+enter) now works for both frontend and backend scripts 2018-03-05 23:19:46 -05:00
azivner
d26170762b inclusion of scripts based on script environment 2018-03-05 23:09:36 -05:00
azivner
b3209a9bbf split javascript mime type into frontend and backend 2018-03-05 22:08:45 -05:00
azivner
61c2456cf6 startNote/currentNote is now accessible on frontend as well 2018-03-04 23:28:26 -05:00
azivner
1c6fc9029f new "disable_inclusion" attribute 2018-03-04 22:09:51 -05:00
azivner
5c91e38dfe server.exec() refactored into api 2018-03-04 21:43:14 -05:00
azivner
07bf075894 cleaned up unused jobs implementation 2018-03-04 21:33:06 -05:00
azivner
ddce5c959e fix render 2018-03-04 21:05:14 -05:00
azivner
3b9d1df05c fixed frontend script execution 2018-03-04 14:21:11 -05:00
azivner
d239ef2956 refactoring of getModules function 2018-03-04 12:06:35 -05:00
azivner
7a865a9081 common JS module system prototype 2018-03-04 10:32:53 -05:00
azivner
83d6c2970f added versioning to the metadata files in export tars 2018-03-03 09:32:21 -05:00
azivner
8c7d159012 fix export/import for multi-valued attributes 2018-03-03 09:30:18 -05:00
azivner
d169f67901 changes in backend script running 2018-03-03 09:11:41 -05:00
azivner
982b723647 basic scheduling of backend scripts using attributes 2018-03-02 20:56:58 -05:00
azivner
31d5ac05ff release 0.8.1 2018-03-01 23:08:53 -05:00
azivner
72d91d1571 don't use eslint on JSON notes, closes #70 2018-03-01 22:42:51 -05:00
azivner
f4b57f4c57 Allow attachments to be included in the scripts, closes #66 2018-03-01 22:30:06 -05:00
azivner
ee0833390a fix export in electron (auth problem) 2018-02-27 09:47:05 -05:00
azivner
2acff07368 release 0.8.0-beta 2018-02-26 22:57:15 -05:00
azivner
bea1d24f07 tweaks to eslint 2018-02-26 22:55:58 -05:00
azivner
adc270c59f removed reference to reddit plugin 2018-02-26 22:31:35 -05:00
azivner
66064f7a94 Script API changes, finished porting reddit plugin, reddit importer tar file 2018-02-26 20:47:34 -05:00
azivner
1501fa8dbf import notes from tar archive, closes #63 2018-02-26 00:07:43 -05:00
azivner
60bba46d80 export subtree to tar file 2018-02-25 10:55:21 -05:00
azivner
12c06ae97e manual transaction handling for jobs 2018-02-24 22:44:45 -05:00
azivner
f0bea9cf71 API changes necessary to port reddit plugin, closes #58 2018-02-24 21:23:04 -05:00
azivner
a555b6319c support for backend jobs and other script API changes 2018-02-24 14:42:52 -05:00
azivner
5dd93e4cdc eslint for javascript inside HTML (htmlmixed mode), closes #62 2018-02-24 00:58:11 -05:00
azivner
3b4509d833 support encryption for files, closes #60 2018-02-23 22:58:24 -05:00
azivner
19308bbfbd small changes to linting and protected session 2018-02-23 20:10:29 -05:00
azivner
4acc5432c3 autocomplete returns items which have at least one of the tokens in the leaf note title, closes #59 2018-02-22 19:52:08 -05:00
azivner
08b8141fdf upgrade to codemirror 5.35 2018-02-21 23:09:52 -05:00
azivner
e1200aa308 lazy loading of eslint only for JS code 2018-02-21 20:30:15 -05:00
azivner
89666eb078 paperclip icon for attachment, closes #61 2018-02-21 19:53:46 -05:00
azivner
d5605aa64d initial support for eslint backed JS linting 2018-02-20 23:24:55 -05:00
azivner
2582b016f9 increased "connection lost" timeout from 5 seconds to 30, it was way to common and mostly false positive 2018-02-20 07:52:39 -05:00
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
azivner
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
225 changed files with 115920 additions and 11380 deletions

14
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="document.db" uuid="a2c75661-f9e2-478f-a69f-6a9409e69997">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url>
<driver-properties>
<property name="enable_load_extension" value="true" />
</driver-properties>
</data-source>
</component>
</project>

View File

@@ -0,0 +1,593 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.8">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
<Visible>1</Visible>
</schema>
<collation id="3" parent="1" name="BINARY"/>
<collation id="4" parent="1" name="NOCASE"/>
<collation id="5" parent="1" name="RTRIM"/>
<table id="6" parent="2" name="api_tokens"/>
<table id="7" parent="2" name="branches"/>
<table id="8" parent="2" name="event_log"/>
<table id="9" parent="2" name="images"/>
<table id="10" parent="2" name="labels"/>
<table id="11" parent="2" name="note_images"/>
<table id="12" parent="2" name="note_revisions"/>
<table id="13" parent="2" name="notes"/>
<table id="14" parent="2" name="options"/>
<table id="15" parent="2" name="recent_notes"/>
<table id="16" parent="2" name="source_ids"/>
<table id="17" parent="2" name="sqlite_master">
<System>1</System>
</table>
<table id="18" parent="2" name="sqlite_sequence">
<System>1</System>
</table>
<table id="19" parent="2" name="sync"/>
<column id="20" parent="6" name="apiTokenId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="21" parent="6" name="token">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="22" parent="6" name="dateCreated">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="23" parent="6" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>apiTokenId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="25" parent="6">
<ColNames>apiTokenId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
</key>
<column id="26" parent="7" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="27" parent="7" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="28" parent="7" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="7" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="7" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="31" parent="7" name="isExpanded">
<Position>6</Position>
<DataType>BOOLEAN|0s</DataType>
</column>
<column id="32" parent="7" name="isDeleted">
<Position>7</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="33" parent="7" name="dateModified">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="34" parent="7" name="sqlite_autoindex_branches_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="36" parent="7" name="IDX_branches_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="37" parent="7">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="38" parent="8" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="39" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="40" parent="8" name="comment">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="41" parent="8" name="dateAdded">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="42" parent="8">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
<column id="43" parent="9" name="imageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="44" parent="9" name="format">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="9" name="checksum">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="9" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="47" parent="9" name="data">
<Position>5</Position>
<DataType>BLOB|0s</DataType>
</column>
<column id="48" parent="9" name="isDeleted">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="49" parent="9" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="50" parent="9" name="dateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="51" parent="9" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="52" parent="9">
<ColNames>imageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key>
<column id="53" parent="10" name="labelId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="54" parent="10" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="55" parent="10" name="name">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="56" parent="10" name="value">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="57" parent="10" name="position">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="58" parent="10" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="59" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="60" parent="10" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="61" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="62" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="63" parent="10" name="IDX_labels_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="64" parent="10">
<ColNames>labelId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key>
<column id="65" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="66" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="67" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="68" parent="11" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="69" parent="11" name="dateModified">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="70" parent="11" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="71" parent="11" name="sqlite_autoindex_note_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteImageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="72" parent="11" name="IDX_note_images_noteId_imageId">
<ColNames>noteId
imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="73" parent="11" name="IDX_note_images_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="74" parent="11" name="IDX_note_images_imageId">
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="75" parent="11">
<ColNames>noteImageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key>
<column id="76" parent="12" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="12" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="12" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="79" parent="12" name="content">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="80" parent="12" name="isProtected">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="81" parent="12" name="dateModifiedFrom">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="82" parent="12" name="dateModifiedTo">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="83" parent="12" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="84" parent="12" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<index id="85" parent="12" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="86" parent="12" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<ColNames>dateModifiedFrom</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="88" parent="12" name="IDX_note_revisions_dateModifiedTo">
<ColNames>dateModifiedTo</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="89" parent="12">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="90" parent="13" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="91" parent="13" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="92" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="93" parent="13" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="94" parent="13" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="95" parent="13" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="96" parent="13" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="97" parent="13" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="98" parent="13" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<index id="99" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="100" parent="13" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="101" parent="13">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="102" parent="14" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="103" parent="14" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="104" parent="14" name="dateModified">
<Position>3</Position>
<DataType>INT|0s</DataType>
</column>
<column id="105" parent="14" name="isSynced">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="106" parent="14" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="107" parent="14">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="108" parent="15" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="109" parent="15" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="110" parent="15" name="dateAccessed">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="111" parent="15" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
</column>
<index id="112" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="113" parent="15">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="114" parent="16" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="115" parent="16" name="dateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="116" parent="16" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="117" parent="16">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="118" parent="17" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="119" parent="17" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="120" parent="17" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="121" parent="17" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="122" parent="17" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="123" parent="18" name="name">
<Position>1</Position>
</column>
<column id="124" parent="18" name="seq">
<Position>2</Position>
</column>
<column id="125" parent="19" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="126" parent="19" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="19" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="128" parent="19" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="129" parent="19" name="syncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="130" parent="19" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="131" parent="19" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="132" parent="19">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>
</database-model>
</dataSource>

View File

@@ -0,0 +1,2 @@
#n:main
!<md> [0, 0, null, null, -2147483648, -2147483648]

View File

@@ -0,0 +1,10 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

9
.idea/jsLinters/jslint.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JSLintConfiguration">
<option devel="true" />
<option es6="true" />
<option maxerr="50" />
<option node="true" />
</component>
</project>

9
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,4 +1,6 @@
# Trilium Notes
[![Join the chat at https://gitter.im/trilium-notes/Lobby](https://badges.gitter.im/trilium-notes/Lobby.svg)](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Trilium Notes is a hierarchical note taking application. Picture tells a thousand words:
![](https://raw.githubusercontent.com/wiki/zadam/trilium/images/screenshot.png)
@@ -10,7 +12,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan
* WYSIWYG (What You See Is What You Get) editing
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
* Seamless note versioning
* Note attributes can be used to tag/label notes as an alternative note organization and querying
* Note labels can be used to tag/label notes as an alternative note organization and querying
* Can be deployed as web application and / or desktop application with offline access (electron based)
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
@@ -35,11 +37,12 @@ List of documentation pages:
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
* [Attributes](https://github.com/zadam/trilium/wiki/Attributes)
* [Labels](https://github.com/zadam/trilium/wiki/Labels)
* [Links](https://github.com/zadam/trilium/wiki/Links)
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization)
* [Document](https://github.com/zadam/trilium/wiki/Document)
* [Theming](https://github.com/zadam/trilium/wiki/Theming)
* [Keyboard shortcuts](https://github.com/zadam/trilium/wiki/Keyboard-shortcuts)
* [Troubleshooting](https://github.com/zadam/trilium/wiki/Troubleshooting)

View File

@@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json
git add package.json
echo 'module.exports = { build_date:"'`date --iso-8601=seconds`'", build_revision: "'`git log -1 --format="%H"`'" };' > services/build.js
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > src/services/build.js
git add services/build.js
git add src/services/build.js
TAG=v$VERSION

View File

@@ -1,3 +1,7 @@
[General]
# Instance name can be used to distinguish between different instances
instanceName=
[Network]
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).

52
db/main_branches.sql Normal file
View File

@@ -0,0 +1,52 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z');

View File

@@ -1,52 +0,0 @@
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z');
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z');

View File

@@ -0,0 +1,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';

View File

@@ -0,0 +1,38 @@
CREATE TABLE "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`branchId`)
);
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified)
SELECT noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified FROM note_tree;
DROP TABLE note_tree;
CREATE INDEX `IDX_branches_noteId` ON `branches` (
`noteId`
);
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
`noteId`,
`parentNoteId`
);
CREATE TABLE `recent_notes_mig` (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
`dateAccessed` TEXT NOT NULL,
isDeleted INT
);
INSERT INTO recent_notes_mig (branchId, notePath, dateAccessed, isDeleted)
SELECT noteTreeId, notePath, dateAccessed, isDeleted FROM recent_notes;
DROP TABLE recent_notes;
ALTER TABLE recent_notes_mig RENAME TO recent_notes;

View File

@@ -0,0 +1,22 @@
create table labels
(
labelId TEXT not null primary key,
noteId TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
);
create index IDX_labels_name_value
on labels (name, value);
create index IDX_labels_noteId
on labels (noteId);
INSERT INTO labels (labelId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;

View File

@@ -0,0 +1 @@
UPDATE options SET name = 'note_revision_snapshot_time_interval' WHERE name = 'history_snapshot_time_interval';

View File

@@ -0,0 +1,14 @@
UPDATE "options" SET "name" = 'passwordVerificationHash' WHERE "name" = 'password_verification_hash';
UPDATE "options" SET "name" = 'dbVersion' WHERE "name" = 'db_version';
UPDATE "options" SET "name" = 'passwordDerivedKeySalt' WHERE "name" = 'password_derived_key_salt';
UPDATE "options" SET "name" = 'documentId' WHERE "name" = 'document_id';
UPDATE "options" SET "name" = 'lastSyncedPull' WHERE "name" = 'last_synced_pull';
UPDATE "options" SET "name" = 'startNotePath' WHERE "name" = 'start_note_path';
UPDATE "options" SET "name" = 'lastSyncedPush' WHERE "name" = 'last_synced_push';
UPDATE "options" SET "name" = 'documentSecret' WHERE "name" = 'document_secret';
UPDATE "options" SET "name" = 'lastBackupDate' WHERE "name" = 'last_backup_date';
UPDATE "options" SET "name" = 'noteRevisionSnapshotTimeInterval' WHERE "name" = 'note_revision_snapshot_time_interval';
UPDATE "options" SET "name" = 'protectedSessionTimeout' WHERE "name" = 'protected_session_timeout';
UPDATE "options" SET "name" = 'encryptedDataKey' WHERE "name" = 'encrypted_data_key';
UPDATE "options" SET "name" = 'encryptedDataKeyIv' WHERE "name" = 'encrypted_data_key_iv';
UPDATE "options" SET "name" = 'passwordVerificationSalt' WHERE "name" = 'password_verification_salt';

View File

@@ -0,0 +1,7 @@
UPDATE labels SET name = 'disableVersioning' WHERE name = 'disable_versioning';
UPDATE labels SET name = 'calendarRoot' WHERE name = 'calendar_root';
UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete';
UPDATE labels SET name = 'excludeFromExport' WHERE name = 'exclude_from_export';
UPDATE labels SET name = 'manualTransactionHandling' WHERE name = 'manual_transaction_handling';
UPDATE labels SET name = 'disableInclusion' WHERE name = 'disable_inclusion';
UPDATE labels SET name = 'appCss' WHERE name = 'app_css';

View File

@@ -0,0 +1,4 @@
UPDATE labels SET name = 'redditId' WHERE name = 'reddit_id';
UPDATE labels SET name = 'redditKind' WHERE name = 'reddit_kind';
UPDATE labels SET name = 'redditCreatedUtc' WHERE name = 'reddit_created_utc';
UPDATE labels SET name = 'redditDateNote' WHERE name = 'reddit_date_note';

View File

@@ -0,0 +1,2 @@
UPDATE labels SET value = 'frontendStartup' WHERE value = 'frontend_startup';
UPDATE labels SET value = 'backendStartup' WHERE value = 'backend_startup';

View File

@@ -0,0 +1,7 @@
UPDATE labels SET name = 'dateData' WHERE name = 'date_data';
UPDATE labels SET name = 'dateNote' WHERE name = 'date_note';
UPDATE labels SET name = 'fileSize' WHERE name = 'file_size';
UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete';
UPDATE labels SET name = 'monthNote' WHERE name = 'month_note';
UPDATE labels SET name = 'originalFileName' WHERE name = 'original_file_name';
UPDATE labels SET name = 'yearNote' WHERE name = 'year_note';

View File

@@ -0,0 +1,5 @@
ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL;
ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL;
UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId);
UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId);

View File

@@ -0,0 +1,34 @@
CREATE TABLE event_logc027
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
INSERT INTO event_logc027(id, noteId, comment, dateAdded) SELECT id, noteId, comment, dateAdded FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_logc027 RENAME TO event_log;
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
INSERT INTO notes_mig (noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime)
SELECT noteId, title, content, isProtected, isDeleted, dateCreated, dateModified, type, mime FROM notes;
DROP TABLE notes;
ALTER TABLE notes_mig RENAME TO notes;
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -9,41 +9,18 @@ CREATE TABLE IF NOT EXISTS "sync" (
`entityId` TEXT NOT NULL,
`sourceId` TEXT NOT NULL,
`syncDate` TEXT NOT NULL);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
`syncDate`
);
CREATE TABLE IF NOT EXISTS "source_ids" (
`sourceId` TEXT NOT NULL,
`dateCreated` TEXT NOT NULL,
PRIMARY KEY(`sourceId`)
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT,
`content` TEXT,
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE TABLE IF NOT EXISTS "event_log" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateAdded` TEXT NOT NULL,
FOREIGN KEY(noteId) REFERENCES notes(noteId)
);
CREATE TABLE IF NOT EXISTS "note_tree" (
`noteTreeId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`noteTreeId`)
);
CREATE TABLE IF NOT EXISTS "note_revisions" (
`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
@@ -52,12 +29,15 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` TEXT NOT NULL
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
CREATE TABLE IF NOT EXISTS "recent_notes" (
`noteTreeId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
`dateAccessed` TEXT NOT NULL,
isDeleted INT
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
`dateModifiedFrom`
);
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
`dateModifiedTo`
);
CREATE TABLE IF NOT EXISTS "images"
(
@@ -79,53 +59,74 @@ CREATE TABLE note_images
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "attributes"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT,
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
`entityName`,
`entityId`
);
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
`syncDate`
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);
CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` (
`noteId`
);
CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` (
`noteId`,
`parentNoteId`
);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
`dateModifiedFrom`
);
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
`dateModifiedTo`
);
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
CREATE TABLE IF NOT EXISTS "api_tokens"
(
apiTokenId TEXT PRIMARY KEY NOT NULL,
token TEXT NOT NULL,
dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0
);
);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
`parentNoteId` TEXT NOT NULL,
`notePosition` INTEGER NOT NULL,
`prefix` TEXT,
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
PRIMARY KEY(`branchId`)
);
CREATE INDEX `IDX_branches_noteId` ON `branches` (
`noteId`
);
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
`noteId`,
`parentNoteId`
);
CREATE TABLE IF NOT EXISTS "recent_notes" (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
`dateAccessed` TEXT NOT NULL,
isDeleted INT
);
CREATE TABLE labels
(
labelId TEXT not null primary key,
noteId TEXT not null,
name TEXT not null,
value TEXT default '' not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
);
CREATE INDEX IDX_labels_name_value
on labels (name, value);
CREATE INDEX IDX_labels_noteId
on labels (noteId);
CREATE TABLE IF NOT EXISTS "event_log"
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
`content` TEXT NOT NULL DEFAULT "",
`isProtected` INT NOT NULL DEFAULT 0,
`isDeleted` INT NOT NULL DEFAULT 0,
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);

View File

@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array
@@ -71,15 +73,15 @@ app.on('ready', () => {
mainWindow = createMainWindow();
const result = globalShortcut.register('CommandOrControl+Alt+P', async () => {
const date_notes = require('./src/services/date_notes');
const utils = require('./src/services/utils');
const dateNoteService = require('./src/services/date_notes');
const dateUtils = require('./src/services/date_utils');
const parentNoteId = await date_notes.getDateNoteId(utils.nowDate());
const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('create-day-sub-note', parentNoteId);
mainWindow.webContents.send('create-day-sub-note', parentNote.noteId);
});
if (!result) {

1543
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.6.2",
"version": "0.12.0",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -12,8 +12,8 @@
"start": "node ./bin/www",
"test-electron": "xo",
"rebuild-electron": "electron-rebuild",
"start-electron": "electron .",
"build-electron": "electron-packager . --out=dist --asar --overwrite --all",
"start-electron": "electron . --disable-gpu",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
@@ -23,47 +23,51 @@
"async-mutex": "^0.1.3",
"axios": "^0.17.1",
"body-parser": "~1.18.2",
"cls-hooked": "^4.2.2",
"cookie-parser": "~1.4.3",
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.5.7",
"electron": "^1.8.2",
"electron": "^2.0.0-beta.5",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"express": "~4.16.2",
"express-promise-wrap": "^0.2.2",
"electron-rebuild": "^1.7.3",
"express": "~4.16.3",
"express-session": "^1.15.6",
"fs-extra": "^4.0.2",
"helmet": "^3.9.0",
"fs-extra": "^4.0.3",
"helmet": "^3.12.0",
"html": "^1.0.0",
"image-type": "^3.0.0",
"imagemin": "^5.3.1",
"imagemin-giflossy": "^5.1.10",
"imagemin-mozjpeg": "^7.0.0",
"imagemin-pngquant": "^5.0.1",
"ini": "^1.3.4",
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.20.1",
"moment": "^2.21.0",
"multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.83.0",
"request": "^2.85.0",
"request-promise": "^4.2.2",
"rimraf": "^2.6.2",
"sanitize-filename": "^1.6.1",
"scrypt": "^6.0.3",
"serve-favicon": "~2.4.5",
"session-file-store": "^1.1.2",
"simple-node-logger": "^0.93.30",
"sqlite": "^2.9.0",
"session-file-store": "^1.2.0",
"simple-node-logger": "^0.93.37",
"sqlite": "^2.9.1",
"tar-stream": "^1.5.5",
"unescape": "^1.0.1",
"ws": "^3.3.2"
"ws": "^3.3.3"
},
"devDependencies": {
"electron-compile": "^6.4.2",
"electron-packager": "^11.0.1",
"electron-prebuilt-compile": "1.8.2",
"electron-rebuild": "^1.7.3",
"tape": "^4.8.0",
"electron-packager": "^11.1.0",
"electron-prebuilt-compile": "2.0.0-beta.5",
"lorem-ipsum": "^1.0.4",
"tape": "^4.9.0",
"xo": "^0.18.0"
},
"config": {

View File

@@ -9,6 +9,8 @@ const session = require('express-session');
const FileStore = require('session-file-store')(session);
const os = require('os');
const sessionSecret = require('./services/session_secret');
const cls = require('./services/cls');
require('./entities/entity_constructor');
const app = express();
@@ -23,6 +25,17 @@ app.use((req, res, next) => {
next();
});
app.use((req, res, next) => {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
cls.init(() => {
cls.namespace.set("Hi");
next();
});
});
app.use(bodyParser.json({limit: '50mb'}));
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
@@ -73,7 +86,7 @@ require('./services/backup');
// trigger consistency checks timer
require('./services/consistency_checks');
require('./plugins/reddit');
require('./services/scheduler');
module.exports = {
app,

23
src/entities/api_token.js Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class ApiToken extends Entity {
static get tableName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
}
}
module.exports = ApiToken;

View File

@@ -1,14 +0,0 @@
"use strict";
const Entity = require('./entity');
class Attribute extends Entity {
static get tableName() { return "attributes"; }
static get primaryKeyName() { return "attributeId"; }
async getNote() {
return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
}
module.exports = Attribute;

32
src/entities/branch.js Normal file
View File

@@ -0,0 +1,32 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
const repository = require('../services/repository');
const sql = require('../services/sql');
class Branch extends Entity {
static get tableName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async beforeSaving() {
super.beforeSaving();
if (this.notePosition === undefined) {
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
}
if (!this.isDeleted) {
this.isDeleted = false;
}
this.dateModified = dateUtils.nowDate()
}
}
module.exports = Branch;

View File

@@ -1,17 +1,26 @@
"use strict";
const utils = require('../services/utils');
const repository = require('../services/repository');
class Entity {
constructor(repository, row) {
utils.assertArguments(repository, row);
this.repository = repository;
constructor(row = {}) {
for (const key in row) {
this[key] = row[key];
}
}
beforeSaving() {
if (!this[this.constructor.primaryKeyName]) {
this[this.constructor.primaryKeyName] = utils.newEntityId();
}
}
async save() {
await repository.updateEntity(this);
return this;
}
}
module.exports = Entity;

View File

@@ -0,0 +1,49 @@
const Note = require('../entities/note');
const NoteRevision = require('../entities/note_revision');
const Image = require('../entities/image');
const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch');
const Label = require('../entities/label');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const repository = require('../services/repository');
function createEntityFromRow(row) {
let entity;
if (row.labelId) {
entity = new Label(row);
}
else if (row.noteRevisionId) {
entity = new NoteRevision(row);
}
else if (row.noteImageId) {
entity = new NoteImage(row);
}
else if (row.imageId) {
entity = new Image(row);
}
else if (row.branchId && row.notePath) {
entity = new RecentNote(row);
}
else if (row.apiTokenId) {
entity = new ApiToken(row);
}
else if (row.branchId) {
entity = new Branch(row);
}
else if (row.noteId) {
entity = new Note(row);
}
else {
throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
}
return entity;
}
repository.setEntityConstructor(createEntityFromRow);
module.exports = {
createEntityFromRow
};

25
src/entities/image.js Normal file
View File

@@ -0,0 +1,25 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class Image extends Entity {
static get tableName() { return "images"; }
static get primaryKeyName() { return "imageId"; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Image;

40
src/entities/label.js Normal file
View File

@@ -0,0 +1,40 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
class Label extends Entity {
static get tableName() { return "labels"; }
static get primaryKeyName() { return "labelId"; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async beforeSaving() {
super.beforeSaving();
if (!this.value) {
// null value isn't allowed
this.value = "";
}
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]);
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Label;

View File

@@ -2,49 +2,162 @@
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
class Note extends Entity {
static get tableName() { return "notes"; }
static get primaryKeyName() { return "noteId"; }
constructor(repository, row) {
super(repository, row);
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNote(this.dataKey, this);
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) {
protected_session.decryptNote(this);
}
if (this.isJson()) {
this.setContent(this.content);
}
setContent(content) {
this.content = content;
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
isJson() {
return this.type === "code" && this.mime === "application/json";
return this.mime === "application/json";
}
async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
}
async getAttribute(name) {
return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
isHtml() {
return (this.type === "code" || this.type === "file") && this.mime === "text/html";
}
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend";
}
if (this.type === 'render') {
return "frontend";
}
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
return "backend";
}
return null;
}
async getLabels() {
return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
async getLabelMap() {
const map = {};
for (const label of await this.getLabels()) {
map[label.name] = label.value;
}
return map;
}
async hasLabel(name) {
const map = await this.getLabelMap();
return map.hasOwnProperty(name);
}
// WARNING: this doesn't take into account the possibility to have multi-valued labels!
async getLabel(name) {
return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]);
}
async getRevisions() {
return this.repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
}
async getTrees() {
return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
async getNoteImages() {
return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
async getBranches() {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
}
async getChildNote(name) {
return await repository.getEntity(`
SELECT notes.*
FROM branches
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
AND branches.parentNoteId = ?
AND notes.title = ?`, [this.noteId, name]);
}
async getChildNotes() {
return await repository.getEntities(`
SELECT notes.*
FROM branches
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND branches.isDeleted = 0
AND branches.parentNoteId = ?
ORDER BY branches.notePosition`, [this.noteId]);
}
async getChildBranches() {
return await repository.getEntities(`
SELECT branches.*
FROM branches
WHERE branches.isDeleted = 0
AND branches.parentNoteId = ?
ORDER BY branches.notePosition`, [this.noteId]);
}
async getParentNotes() {
return await repository.getEntities(`
SELECT parent_notes.*
FROM
branches AS child_tree
JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId
WHERE child_tree.noteId = ?
AND child_tree.isDeleted = 0
AND parent_notes.isDeleted = 0`, [this.noteId]);
}
beforeSaving() {
this.content = JSON.stringify(this.jsonContent, null, '\t');
super.beforeSaving();
if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t');
}
if (this.isProtected) {
protected_session.encryptNote(this.dataKey, this);
protected_session.encryptNote(this);
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}

View File

@@ -0,0 +1,34 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
class NoteImage extends Entity {
static get tableName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
async getImage() {
return await repository.getEntity("SELECT * FROM images WHERE imageId = ?", [this.imageId]);
}
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = NoteImage;

View File

@@ -1,13 +1,32 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const utils = require('../services/utils');
const repository = require('../services/repository');
class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNoteRevision(this);
}
}
async getNote() {
return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
}
beforeSaving() {
super.beforeSaving();
if (this.isProtected) {
protected_session.encryptNoteRevision(this);
}
}
}

View File

@@ -1,18 +0,0 @@
"use strict";
const Entity = require('./entity');
class NoteTree extends Entity {
static get tableName() { return "note_tree"; }
static get primaryKeyName() { return "noteTreeId"; }
async getNote() {
return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
}
async getParentNote() {
return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND parentNoteId = ?", [this.parentNoteId]);
}
}
module.exports = NoteTree;

View File

@@ -0,0 +1,10 @@
"use strict";
const Entity = require('./entity');
class RecentNote extends Entity {
static get tableName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; }
}
module.exports = RecentNote;

View File

@@ -1,144 +0,0 @@
"use strict";
const sql = require('../services/sql');
const notes = require('../services/notes');
const axios = require('axios');
const log = require('../services/log');
const utils = require('../services/utils');
const unescape = require('unescape');
const attributes = require('../services/attributes');
const sync_mutex = require('../services/sync_mutex');
const config = require('../services/config');
const date_notes = require('../services/date_notes');
// "reddit" date note is subnote of date note which contains all reddit comments from that date
const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note';
async function createNote(parentNoteId, noteTitle, noteText) {
return (await notes.createNewNote(parentNoteId, {
title: noteTitle,
content: noteText,
target: 'into',
isProtected: false
})).noteId;
}
function redditId(kind, id) {
return kind + "_" + id;
}
async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
const dateStr = dateTimeStr.substr(0, 10);
let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr);
if (!redditDateNoteId) {
const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId);
redditDateNoteId = await createNote(dateNoteId, "Reddit");
await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
}
return redditDateNoteId;
}
async function importComments(rootNoteId, accountName, afterId = null) {
let url = `https://www.reddit.com/user/${accountName}.json`;
if (afterId) {
url += "?after=" + afterId;
}
const response = await axios.get(url);
const listing = response.data;
if (listing.kind !== 'Listing') {
log.info(`Reddit: Unknown object kind ${listing.kind}`);
return;
}
const children = listing.data.children;
let importedComments = 0;
for (const child of children) {
const comment = child.data;
let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id));
if (commentNoteId) {
continue;
}
const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000));
const permaLink = 'https://reddit.com' + comment.permalink;
const noteText =
`<p><a href="${permaLink}">${permaLink}</a></p>
<p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,
subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,
karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>`
+ unescape(comment.body_html);
let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId);
await sql.doInTransaction(async () => {
commentNoteId = await createNote(parentNoteId, comment.link_title, noteText);
log.info("Reddit: Imported comment to note " + commentNoteId);
importedComments++;
await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind);
await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id));
await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc);
});
}
// if there have been no imported comments on this page, there shouldn't be any to import
// on the next page since those are older
if (listing.data.after && importedComments > 0) {
importedComments += await importComments(rootNoteId, accountName, listing.data.after);
}
return importedComments;
}
let redditAccounts = [];
async function runImport() {
const rootNoteId = await date_notes.getRootNoteId();
// technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import
// concurrently with sync
await sync_mutex.doExclusively(async () => {
let importedComments = 0;
for (const account of redditAccounts) {
importedComments += await importComments(rootNoteId, account);
}
log.info(`Reddit: Imported ${importedComments} comments.`);
});
}
sql.dbReady.then(async () => {
if (!config['Reddit'] || config['Reddit']['enabled'] !== true) {
return;
}
const redditAccountsStr = config['Reddit']['accounts'];
if (!redditAccountsStr) {
log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'");
}
redditAccounts = redditAccountsStr.split(",").map(s => s.trim());
const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600);
setInterval(runImport, pollingIntervalInSeconds * 1000);
setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -1,21 +0,0 @@
const api = (function() {
const pluginButtonsEl = $("#plugin-buttons");
async function activateNote(notePath) {
await noteTree.activateNode(notePath);
}
function addButtonToToolbar(buttonId, button) {
$("#" + buttonId).remove();
button.attr('id', buttonId);
pluginButtonsEl.append(button);
}
return {
addButtonToToolbar,
activateNote
}
})();

View File

@@ -1,33 +0,0 @@
"use strict";
const cloning = (function() {
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
prefix: prefix
});
if (!resp.success) {
alert(resp.message);
return;
}
await noteTree.reload();
}
// beware that first arg is noteId and second is noteTreeId!
async function cloneNoteAfter(noteId, afterNoteTreeId) {
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId);
if (!resp.success) {
alert(resp.message);
return;
}
await noteTree.reload();
}
return {
cloneNoteAfter,
cloneNoteTo
};
})();

View File

@@ -1,164 +0,0 @@
"use strict";
const contextMenu = (function() {
const treeEl = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
async function pasteAfter(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChanges.moveAfterNode(nodes, node);
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteAfter(noteId, node.data.noteTreeId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
throwError("Unrecognized clipboard mode=" + clipboardMode);
}
}
async function pasteInto(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChanges.moveToNode(nodes, node);
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloning.cloneNoteTo(noteId, node.data.noteId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardMode = 'copy';
showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes.map(node => node.key);
clipboardMode = 'cut';
showMessage("Note(s) have been cut into clipboard.");
}
const contextMenuSettings = {
delegate: "span.fancytree-title",
autoFocus: true,
menu: [
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
{title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
{title: "----"},
{title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"},
{title: "----"},
{title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"},
{title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"},
{title: "----"},
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
],
beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click
node.setActive();
// Disable tree keyboard handling
ui.menu.prevKeyboard = node.tree.options.keyboard;
node.tree.options.keyboard = false;
},
close: (event, ui) => {},
select: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
if (ui.cmd === "insertNoteHere") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
noteTree.createNote(node, parentNoteId, 'after', isProtected);
}
else if (ui.cmd === "insertChildNote") {
noteTree.createNote(node, node.data.noteId, 'into');
}
else if (ui.cmd === "editTreePrefix") {
editTreePrefix.showDialog(node);
}
else if (ui.cmd === "protectSubTree") {
protected_session.protectSubTree(node.data.noteId, true);
}
else if (ui.cmd === "unprotectSubTree") {
protected_session.protectSubTree(node.data.noteId, false);
}
else if (ui.cmd === "copy") {
copy(noteTree.getSelectedNodes());
}
else if (ui.cmd === "cut") {
cut(noteTree.getSelectedNodes());
}
else if (ui.cmd === "pasteAfter") {
pasteAfter(node);
}
else if (ui.cmd === "pasteInto") {
pasteInto(node);
}
else if (ui.cmd === "delete") {
treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
}
else if (ui.cmd === "collapse-sub-tree") {
noteTree.collapseTree(node);
}
else if (ui.cmd === "force-note-sync") {
forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sort-alphabetically") {
noteTree.sortAlphabetically(node.data.noteId);
}
else {
messaging.logError("Unknown command: " + ui.cmd);
}
}
};
return {
pasteAfter,
pasteInto,
cut,
copy,
contextMenuSettings
}
})();

View File

@@ -1,137 +1,133 @@
"use strict";
import cloningService from '../services/cloning.js';
import linkService from '../services/link.js';
import noteDetailService from '../services/note_detail.js';
import treeUtils from '../services/tree_utils.js';
import autocompleteService from '../services/autocomplete.js';
const addLink = (function() {
const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form");
const $autoComplete = $("#note-autocomplete");
const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form");
const $autoComplete = $("#note-autocomplete");
const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
function setLinkType(linkType) {
$linkTypes.each(function () {
$(this).prop('checked', $(this).val() === linkType);
});
linkTypeChanged();
}
function showDialog() {
glob.activeDialog = $dialog;
if (noteEditor.getCurrentNoteType() === 'text') {
$linkTypeHtml.prop('disabled', false);
setLinkType('html');
}
else {
$linkTypeHtml.prop('disabled', true);
setLinkType('selected-to-current');
}
$dialog.dialog({
modal: true,
width: 700
});
$autoComplete.val('').focus();
$clonePrefix.val('');
$linkTitle.val('');
function setDefaultLinkTitle(noteId) {
const noteTitle = noteTree.getNoteTitle(noteId);
$linkTitle.val(noteTitle);
}
$autoComplete.autocomplete({
source: noteTree.getAutocompleteItems(),
minLength: 0,
change: () => {
const val = $autoComplete.val();
const notePath = link.getNodePathFromLabel(val);
if (!notePath) {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
}
},
// this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: (event, ui) => {
const notePath = link.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
setDefaultLinkTitle(noteId);
}
});
}
$form.submit(() => {
const value = $autoComplete.val();
const notePath = link.getNodePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) {
const linkType = $("input[name='add-link-type']:checked").val();
if (linkType === 'html') {
const linkTitle = $linkTitle.val();
$dialog.dialog("close");
link.addLinkToEditor(linkTitle, '#' + notePath);
}
else if (linkType === 'selected-to-current') {
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
$dialog.dialog("close");
}
else if (linkType === 'current-to-selected') {
const prefix = $clonePrefix.val();
cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
$dialog.dialog("close");
}
}
return false;
function setLinkType(linkType) {
$linkTypes.each(function () {
$(this).prop('checked', $(this).val() === linkType);
});
function linkTypeChanged() {
const value = $linkTypes.filter(":checked").val();
linkTypeChanged();
}
if (value === 'html') {
$linkTitleFormGroup.show();
$prefixFormGroup.hide();
async function showDialog() {
glob.activeDialog = $dialog;
if (noteDetailService.getCurrentNoteType() === 'text') {
$linkTypeHtml.prop('disabled', false);
setLinkType('html');
}
else {
$linkTypeHtml.prop('disabled', true);
setLinkType('selected-to-current');
}
$dialog.dialog({
modal: true,
width: 700
});
$autoComplete.val('').focus();
$clonePrefix.val('');
$linkTitle.val('');
async function setDefaultLinkTitle(noteId) {
const noteTitle = await treeUtils.getNoteTitle(noteId);
$linkTitle.val(noteTitle);
}
$autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(),
minLength: 0,
change: async () => {
const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val);
if (!notePath) {
return;
}
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
await setDefaultLinkTitle(noteId);
}
},
// this is called when user goes through autocomplete list with keyboard
// at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
focus: async (event, ui) => {
const notePath = linkService.getNodePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
await setDefaultLinkTitle(noteId);
}
else {
$linkTitleFormGroup.hide();
$prefixFormGroup.show();
});
}
$form.submit(() => {
const value = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) {
const linkType = $("input[name='add-link-type']:checked").val();
if (linkType === 'html') {
const linkTitle = $linkTitle.val();
$dialog.dialog("close");
linkService.addLinkToEditor(linkTitle, '#' + notePath);
}
else if (linkType === 'selected-to-current') {
const prefix = $clonePrefix.val();
cloningService.cloneNoteTo(noteId, noteDetailService.getCurrentNoteId(), prefix);
$dialog.dialog("close");
}
else if (linkType === 'current-to-selected') {
const prefix = $clonePrefix.val();
cloningService.cloneNoteTo(noteDetailService.getCurrentNoteId(), noteId, prefix);
$dialog.dialog("close");
}
}
$linkTypes.change(linkTypeChanged);
return false;
});
$(document).bind('keydown', 'ctrl+l', e => {
showDialog();
function linkTypeChanged() {
const value = $linkTypes.filter(":checked").val();
e.preventDefault();
});
if (value === 'html') {
$linkTitleFormGroup.show();
$prefixFormGroup.hide();
}
else {
$linkTitleFormGroup.hide();
$prefixFormGroup.show();
}
}
return {
showDialog
};
})();
$linkTypes.change(linkTypeChanged);
export default {
showDialog
};

View File

@@ -1,224 +0,0 @@
"use strict";
const attributesDialog = (function() {
const $dialog = $("#attributes-dialog");
const $saveAttributesButton = $("#save-attributes-button");
const $attributesBody = $('#attributes-table tbody');
const attributesModel = new AttributesModel();
let attributeNames = [];
function AttributesModel() {
const self = this;
this.attributes = ko.observableArray();
this.loadAttributes = async function() {
const noteId = noteEditor.getCurrentNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
self.attributes(attributes.map(ko.observable));
addLastEmptyRow();
attributeNames = await server.get('attributes/names');
// attribute might not be rendered immediatelly so could not focus
setTimeout(() => $(".attribute-name:last").focus(), 100);
$attributesBody.sortable({
handle: '.handle',
containment: $attributesBody,
update: function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// attributes in the viewmodel (self.attributes()) stays the same
$attributesBody.find('input[name="position"]').each(function() {
const attr = self.getTargetAttribute(this);
attr().position = position++;
});
}
});
};
this.deleteAttribute = function(data, event) {
const attr = self.getTargetAttribute(event.target);
const attrData = attr();
if (attrData) {
attrData.isDeleted = 1;
attr(attrData);
addLastEmptyRow();
}
};
function isValid() {
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
if (self.isEmptyName(i)) {
return false;
}
}
return true;
}
this.save = async function() {
// we need to defocus from input (in case of enter-triggered save) because value is updated
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveAttributesButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
const noteId = noteEditor.getCurrentNoteId();
const attributesToSave = self.attributes()
.map(attr => attr())
.filter(attr => attr.attributeId !== "" || attr.name !== "");
const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
self.attributes(attributes.map(ko.observable));
addLastEmptyRow();
showMessage("Attributes have been saved.");
noteEditor.loadAttributeList();
};
function addLastEmptyRow() {
const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.attributes.push(ko.observable({
attributeId: '',
name: '',
value: '',
isDeleted: 0,
position: 0
}));
}
}
this.attributeChanged = function (data, event) {
addLastEmptyRow();
const attr = self.getTargetAttribute(event.target);
attr.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.attributes()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
const attr = attrs[i]();
if (index !== i && cur.name === attr.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.attributes()[index]();
return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
};
this.getTargetAttribute = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.attributes()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await attributesModel.loadAttributes();
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
}
$(document).bind('keydown', 'alt+a', e => {
showDialog();
e.preventDefault();
});
ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
$(document).on('focus', '.attribute-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: attributeNames.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$(document).on('focus', '.attribute-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const attributeName = $(this).parent().parent().find('.attribute-name').val();
if (attributeName.trim() === "") {
return;
}
const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
if (attributeValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in init.js
source: attributeValues.map(attr => {
return {
label: attr,
value: attr
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
return {
showDialog
};
})();

View File

@@ -0,0 +1,51 @@
import treeService from '../services/tree.js';
import server from '../services/server.js';
import treeCache from "../services/tree_cache.js";
import treeUtils from "../services/tree_utils.js";
const $dialog = $("#edit-tree-prefix-dialog");
const $form = $("#edit-tree-prefix-form");
const $treePrefixInput = $("#tree-prefix-input");
const $noteTitle = $('#tree-prefix-note-title');
let branchId;
async function showDialog() {
glob.activeDialog = $dialog;
await $dialog.dialog({
modal: true,
width: 500
});
const currentNode = treeService.getCurrentNode();
branchId = currentNode.data.branchId;
const branch = await treeCache.getBranch(branchId);
$treePrefixInput.val(branch.prefix).focus();
const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}
async function savePrefix() {
const prefix = $treePrefixInput.val();
await server.put('branches/' + branchId + '/set-prefix', { prefix: prefix });
await treeService.setPrefix(branchId, prefix);
$dialog.dialog("close");
}
$form.submit(() => {
savePrefix();
return false;
});
export default {
showDialog
};

View File

@@ -1,45 +0,0 @@
"use strict";
const editTreePrefix = (function() {
const $dialog = $("#edit-tree-prefix-dialog");
const $form = $("#edit-tree-prefix-form");
const $treePrefixInput = $("#tree-prefix-input");
const $noteTitle = $('#tree-prefix-note-title');
let noteTreeId;
async function showDialog() {
glob.activeDialog = $dialog;
await $dialog.dialog({
modal: true,
width: 500
});
const currentNode = noteTree.getCurrentNode();
noteTreeId = currentNode.data.noteTreeId;
$treePrefixInput.val(currentNode.data.prefix).focus();
const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}
$form.submit(() => {
const prefix = $treePrefixInput.val();
server.put('tree/' + noteTreeId + '/set-prefix', {
prefix: prefix
}).then(() => noteTree.setPrefix(noteTreeId, prefix));
$dialog.dialog("close");
return false;
});
return {
showDialog
};
})();

View File

@@ -1,38 +1,38 @@
"use strict";
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
const eventLog = (function() {
const $dialog = $("#event-log-dialog");
const $list = $("#event-log-list");
const $dialog = $("#event-log-dialog");
const $list = $("#event-log-list");
async function showDialog() {
glob.activeDialog = $dialog;
async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
const result = await server.get('event-log');
const result = await server.get('event-log');
$list.html('');
$list.html('');
for (const event of result) {
const dateTime = formatDateTime(parseDate(event.dateAdded));
for (const event of result) {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
if (event.noteId) {
const noteLink = link.createNoteLink(event.noteId).prop('outerHTML');
if (event.noteId) {
const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
event.comment = event.comment.replace('<note>', noteLink);
}
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
$list.append(eventEl);
event.comment = event.comment.replace('<note>', noteLink);
}
}
return {
showDialog
};
})();
const eventEl = $('<li>').html(dateTime + " - " + event.comment);
$list.append(eventEl);
}
}
export default {
showDialog
};

View File

@@ -1,56 +1,49 @@
"use strict";
import treeService from '../services/tree.js';
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import autocompleteService from '../services/autocomplete.js';
const jumpToNote = (function() {
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
async function showDialog() {
glob.activeDialog = $dialog;
async function showDialog() {
glob.activeDialog = $dialog;
$autoComplete.val('');
$autoComplete.val('');
$dialog.dialog({
modal: true,
width: 800
});
await $autoComplete.autocomplete({
source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
minLength: 0
});
}
function getSelectedNotePath() {
const val = $autoComplete.val();
return link.getNodePathFromLabel(val);
}
function goToNote() {
const notePath = getSelectedNotePath();
if (notePath) {
noteTree.activateNode(notePath);
$dialog.dialog('close');
}
}
$(document).bind('keydown', 'ctrl+j', e => {
showDialog();
e.preventDefault();
$dialog.dialog({
modal: true,
width: 800
});
$form.submit(() => {
const action = $dialog.find("button:focus").val();
goToNote();
return false;
await $autoComplete.autocomplete({
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
minLength: 1
});
}
return {
showDialog
};
})();
function getSelectedNotePath() {
const val = $autoComplete.val();
return linkService.getNodePathFromLabel(val);
}
function goToNote() {
const notePath = getSelectedNotePath();
if (notePath) {
treeService.activateNode(notePath);
$dialog.dialog('close');
}
}
$form.submit(() => {
goToNote();
return false;
});
export default {
showDialog
};

View File

@@ -0,0 +1,223 @@
import noteDetailService from '../services/note_detail.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
const $dialog = $("#labels-dialog");
const $saveLabelsButton = $("#save-labels-button");
const $labelsBody = $('#labels-table tbody');
const labelsModel = new LabelsModel();
let labelNames = [];
function LabelsModel() {
const self = this;
this.labels = ko.observableArray();
this.updateLabelPositions = function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// labels in the viewmodel (self.labels()) stays the same
$labelsBody.find('input[name="position"]').each(function() {
const label = self.getTargetLabel(this);
label().position = position++;
});
};
this.loadLabels = async function() {
const noteId = noteDetailService.getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels');
self.labels(labels.map(ko.observable));
addLastEmptyRow();
labelNames = await server.get('labels/names');
// label might not be rendered immediatelly so could not focus
setTimeout(() => $(".label-name:last").focus(), 100);
$labelsBody.sortable({
handle: '.handle',
containment: $labelsBody,
update: this.updateLabelPositions
});
};
this.deleteLabel = function(data, event) {
const label = self.getTargetLabel(event.target);
const labelData = label();
if (labelData) {
labelData.isDeleted = 1;
label(labelData);
addLastEmptyRow();
}
};
function isValid() {
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
if (self.isEmptyName(i)) {
return false;
}
}
return true;
}
this.save = async function() {
// we need to defocus from input (in case of enter-triggered save) because value is updated
// on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
// stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
$saveLabelsButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateLabelPositions();
const noteId = noteDetailService.getCurrentNoteId();
const labelsToSave = self.labels()
.map(label => label())
.filter(label => label.labelId !== "" || label.name !== "");
const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
self.labels(labels.map(ko.observable));
addLastEmptyRow();
infoService.showMessage("Labels have been saved.");
noteDetailService.loadLabelList();
};
function addLastEmptyRow() {
const labels = self.labels().filter(attr => attr().isDeleted === 0);
const last = labels.length === 0 ? null : labels[labels.length - 1]();
if (!last || last.name.trim() !== "" || last.value !== "") {
self.labels.push(ko.observable({
labelId: '',
name: '',
value: '',
isDeleted: 0,
position: 0
}));
}
}
this.labelChanged = function (data, event) {
addLastEmptyRow();
const label = self.getTargetLabel(event.target);
label.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.labels()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let labels = self.labels(), i = 0; i < labels.length; i++) {
const label = labels[i]();
if (index !== i && cur.name === label.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.labels()[index]();
return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
};
this.getTargetLabel = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.labels()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await labelsModel.loadLabels();
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
}
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
$(document).on('focus', '.label-name', function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: labelNames.map(label => {
return {
label: label,
value: label
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
$(document).on('focus', '.label-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const labelName = $(this).parent().parent().find('.label-name').val();
if (labelName.trim() === "") {
return;
}
const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
if (labelValues.length === 0) {
return;
}
$(this).autocomplete({
// shouldn't be required and autocomplete should just accept array of strings, but that fails
// because we have overriden filter() function in autocomplete.js
source: labelValues.map(label => {
return {
label: label,
value: label
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
export default {
showDialog
};

View File

@@ -1,78 +0,0 @@
"use strict";
const noteHistory = (function() {
const $dialog = $("#note-history-dialog");
const $list = $("#note-history-list");
const $content = $("#note-history-content");
const $title = $("#note-history-title");
let historyItems = [];
async function showCurrentNoteHistory() {
await showNoteHistoryDialog(noteEditor.getCurrentNoteId());
}
async function showNoteHistoryDialog(noteId, noteRevisionId) {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$list.empty();
$content.empty();
historyItems = await server.get('notes-history/' + noteId);
for (const item of historyItems) {
const dateModified = parseDate(item.dateModifiedFrom);
$list.append($('<option>', {
value: item.noteRevisionId,
text: formatDateTime(dateModified)
}));
}
if (historyItems.length > 0) {
if (!noteRevisionId) {
noteRevisionId = $list.find("option:first").val();
}
$list.val(noteRevisionId).trigger('change');
}
else {
$title.text("No history for this note yet...");
}
}
$(document).bind('keydown', 'alt+h', e => {
showCurrentNoteHistory();
e.preventDefault();
});
$list.on('change', () => {
const optVal = $list.find(":selected").val();
const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
$title.html(historyItem.title);
$content.html(historyItem.content);
});
$(document).on('click', "a[action='note-history']", event => {
const linkEl = $(event.target);
const noteId = linkEl.attr('note-path');
const noteRevisionId = linkEl.attr('note-history-id');
showNoteHistoryDialog(noteId, noteRevisionId);
return false;
});
return {
showCurrentNoteHistory
};
})();

View File

@@ -0,0 +1,78 @@
import noteDetailService from '../services/note_detail.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
const $dialog = $("#note-revisions-dialog");
const $list = $("#note-revision-list");
const $content = $("#note-revision-content");
const $title = $("#note-revision-title");
let revisionItems = [];
async function showCurrentNoteRevisions() {
await showNoteRevisionsDialog(noteDetailService.getCurrentNoteId());
}
async function showNoteRevisionsDialog(noteId, noteRevisionId) {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$list.empty();
$content.empty();
revisionItems = await server.get('notes/' + noteId + '/revisions');
for (const item of revisionItems) {
const dateModified = utils.parseDate(item.dateModifiedFrom);
$list.append($('<option>', {
value: item.noteRevisionId,
text: utils.formatDateTime(dateModified)
}));
}
if (revisionItems.length > 0) {
if (!noteRevisionId) {
noteRevisionId = $list.find("option:first").val();
}
$list.val(noteRevisionId).trigger('change');
}
else {
$title.text("No revisions for this note yet...");
}
}
$list.on('change', () => {
const optVal = $list.find(":selected").val();
const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal);
$title.html(revisionItem.title);
if (revisionItem.type === 'text') {
$content.html(revisionItem.content);
}
else if (revisionItem.type === 'code') {
$content.html($("<pre>").text(revisionItem.content));
}
});
$(document).on('click', "a[action='note-revision']", event => {
const linkEl = $(event.target);
const noteId = linkEl.attr('note-path');
const noteRevisionId = linkEl.attr('note-revision-id');
showNoteRevisionsDialog(noteId, noteRevisionId);
return false;
});
export default {
showCurrentNoteRevisions
};

View File

@@ -1,57 +1,49 @@
"use strict";
import noteDetailService from '../services/note_detail.js';
const noteSource = (function() {
const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
const $dialog = $("#note-source-dialog");
const $noteSource = $("#note-source");
function showDialog() {
glob.activeDialog = $dialog;
function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
const noteText = noteEditor.getCurrentNote().detail.content;
$noteSource.text(formatHtml(noteText));
}
function formatHtml(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return formatNode(div, 0).innerHTML.trim();
}
function formatNode(node, level) {
const indentBefore = new Array(level++ + 1).join(' ');
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (let i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);
formatNode(node.children[i], level);
if (node.lastElementChild === node.children[i]) {
textNode = document.createTextNode('\n' + indentAfter);
node.appendChild(textNode);
}
}
return node;
}
$(document).bind('keydown', 'ctrl+u', e => {
showDialog();
e.preventDefault();
$dialog.dialog({
modal: true,
width: 800,
height: 500
});
return {
showDialog
};
})();
const noteText = noteDetailService.getCurrentNote().content;
$noteSource.text(formatHtml(noteText));
}
function formatHtml(str) {
const div = document.createElement('div');
div.innerHTML = str.trim();
return formatNode(div, 0).innerHTML.trim();
}
function formatNode(node, level) {
const indentBefore = new Array(level++ + 1).join(' ');
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (const i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);
formatNode(node.children[i], level);
if (node.lastElementChild === node.children[i]) {
textNode = document.createTextNode('\n' + indentAfter);
node.appendChild(textNode);
}
}
return node;
}
export default {
showDialog
};

View File

@@ -1,57 +1,56 @@
"use strict";
const settings = (function() {
const $dialog = $("#settings-dialog");
const $tabs = $("#settings-tabs");
import protectedSessionHolder from '../services/protected_session_holder.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
const settingModules = [];
const $dialog = $("#options-dialog");
const $tabs = $("#options-tabs");
function addModule(module) {
settingModules.push(module);
}
const tabHandlers = [];
async function showDialog() {
glob.activeDialog = $dialog;
function addTabHandler(handler) {
tabHandlers.push(handler);
}
const settings = await server.get('settings');
async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 900
});
const options = await server.get('options');
$tabs.tabs();
$dialog.dialog({
modal: true,
width: 900
});
for (const module of settingModules) {
if (module.settingsLoaded) {
module.settingsLoaded(settings);
}
$tabs.tabs();
for (const handler of tabHandlers) {
if (handler.optionsLoaded) {
handler.optionsLoaded(options);
}
}
}
async function saveSettings(settingName, settingValue) {
await server.post('settings', {
name: settingName,
value: settingValue
});
async function saveOptions(optionName, optionValue) {
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
showMessage("Settings change have been saved.");
}
infoService.showMessage("Options change have been saved.");
}
return {
showDialog,
saveSettings,
addModule
};
})();
export default {
showDialog,
saveOptions
};
settings.addModule((function() {
addTabHandler((function() {
const $form = $("#change-password-form");
const $oldPassword = $("#old-password");
const $newPassword1 = $("#new-password1");
const $newPassword2 = $("#new-password2");
function settingsLoaded(settings) {
function optionsLoaded(options) {
}
$form.submit(() => {
@@ -76,10 +75,10 @@ settings.addModule((function() {
alert("Password has been changed. Trilium will be reloaded after you press OK.");
// password changed so current protected session is invalid and needs to be cleared
protected_session.resetProtectedSession();
protectedSessionHolder.resetProtectedSession();
}
else {
showError(result.message);
infoService.showError(result.message);
}
});
@@ -87,55 +86,55 @@ settings.addModule((function() {
});
return {
settingsLoaded
optionsLoaded
};
})());
settings.addModule((function() {
addTabHandler((function() {
const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const settingName = 'protected_session_timeout';
const optionName = 'protectedSessionTimeout';
function settingsLoaded(settings) {
$protectedSessionTimeout.val(settings[settingName]);
function optionsLoaded(options) {
$protectedSessionTimeout.val(options[optionName]);
}
$form.submit(() => {
const protectedSessionTimeout = $protectedSessionTimeout.val();
settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
saveOptions(optionName, protectedSessionTimeout).then(() => {
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
});
return false;
});
return {
settingsLoaded
optionsLoaded
};
})());
settings.addModule((function () {
const $form = $("#history-snapshot-time-interval-form");
const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
const settingName = 'history_snapshot_time_interval';
addTabHandler((function () {
const $form = $("#note-revision-snapshot-time-interval-form");
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
const optionName = 'noteRevisionSnapshotTimeInterval';
function settingsLoaded(settings) {
$timeInterval.val(settings[settingName]);
function optionsLoaded(options) {
$timeInterval.val(options[optionName]);
}
$form.submit(() => {
settings.saveSettings(settingName, $timeInterval.val());
saveOptions(optionName, $timeInterval.val());
return false;
});
return {
settingsLoaded
optionsLoaded
};
})());
settings.addModule((async function () {
addTabHandler((async function () {
const $appVersion = $("#app-version");
const $dbVersion = $("#db-version");
const $buildDate = $("#build-date");
@@ -143,16 +142,16 @@ settings.addModule((async function () {
const appInfo = await server.get('app-info');
$appVersion.html(appInfo.app_version);
$dbVersion.html(appInfo.db_version);
$buildDate.html(appInfo.build_date);
$buildRevision.html(appInfo.build_revision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
$appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion);
$buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
return {};
})());
settings.addModule((async function () {
addTabHandler((async function () {
const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button");
const $anonymizeButton = $("#anonymize-button");
@@ -163,27 +162,27 @@ settings.addModule((async function () {
$forceFullSyncButton.click(async () => {
await server.post('sync/force-full-sync');
showMessage("Full sync triggered");
infoService.showMessage("Full sync triggered");
});
$fillSyncRowsButton.click(async () => {
await server.post('sync/fill-sync-rows');
showMessage("Sync rows filled successfully");
infoService.showMessage("Sync rows filled successfully");
});
$anonymizeButton.click(async () => {
await server.post('anonymization/anonymize');
showMessage("Created anonymized database");
infoService.showMessage("Created anonymized database");
});
$cleanupSoftDeletedButton.click(async () => {
if (confirm("Do you really want to clean up soft-deleted items?")) {
await server.post('cleanup/cleanup-soft-deleted-items');
showMessage("Soft deleted items have been cleaned up");
infoService.showMessage("Soft deleted items have been cleaned up");
}
});
@@ -191,14 +190,14 @@ settings.addModule((async function () {
if (confirm("Do you really want to clean up unused images?")) {
await server.post('cleanup/cleanup-unused-images');
showMessage("Unused images have been cleaned up");
infoService.showMessage("Unused images have been cleaned up");
}
});
$vacuumDatabaseButton.click(async () => {
await server.post('cleanup/vacuum-database');
showMessage("Database has been vacuumed");
infoService.showMessage("Database has been vacuumed");
});
return {};

View File

@@ -1,89 +1,87 @@
"use strict";
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
const recentChanges = (function() {
const $dialog = $("#recent-changes-dialog");
const $dialog = $("#recent-changes-dialog");
async function showDialog() {
glob.activeDialog = $dialog;
async function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
$dialog.dialog({
modal: true,
width: 800,
height: 700
});
const result = await server.get('recent-changes/');
const result = await server.get('recent-changes/');
$dialog.html('');
$dialog.html('');
const groupedByDate = groupByDate(result);
const groupedByDate = groupByDate(result);
for (const [dateDay, dayChanges] of groupedByDate) {
const changesListEl = $('<ul>');
for (const [dateDay, dayChanges] of groupedByDate) {
const changesListEl = $('<ul>');
const dayEl = $('<div>').append($('<b>').html(formatDate(dateDay))).append(changesListEl);
const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl);
for (const change of dayChanges) {
const formattedTime = formatTime(parseDate(change.dateModifiedTo));
for (const change of dayChanges) {
const formattedTime = utils.formatTime(utils.parseDate(change.dateModifiedTo));
const revLink = $("<a>", {
href: 'javascript:',
text: 'rev'
}).attr('action', 'note-history')
.attr('note-path', change.noteId)
.attr('note-history-id', change.noteRevisionId);
const revLink = $("<a>", {
href: 'javascript:',
text: 'rev'
}).attr('action', 'note-revision')
.attr('note-path', change.noteId)
.attr('note-revision-id', change.noteRevisionId);
let noteLink;
let noteLink;
if (change.current_isDeleted) {
noteLink = change.current_title;
}
else {
noteLink = link.createNoteLink(change.noteId, change.title);
}
changesListEl.append($('<li>')
.append(formattedTime + ' - ')
.append(noteLink)
.append(' (').append(revLink).append(')'));
}
$dialog.append(dayEl);
}
}
function groupByDate(result) {
const groupedByDate = new Map();
const dayCache = {};
for (const row of result) {
let dateDay = parseDate(row.dateModifiedTo);
dateDay.setHours(0);
dateDay.setMinutes(0);
dateDay.setSeconds(0);
dateDay.setMilliseconds(0);
// this stupidity is to make sure that we always use the same day object because Map uses only
// reference equality
if (dayCache[dateDay]) {
dateDay = dayCache[dateDay];
if (change.current_isDeleted) {
noteLink = change.current_title;
}
else {
dayCache[dateDay] = dateDay;
noteLink = linkService.createNoteLink(change.noteId, change.title);
}
if (!groupedByDate.has(dateDay)) {
groupedByDate.set(dateDay, []);
}
groupedByDate.get(dateDay).push(row);
changesListEl.append($('<li>')
.append(formattedTime + ' - ')
.append(noteLink)
.append(' (').append(revLink).append(')'));
}
return groupedByDate;
$dialog.append(dayEl);
}
}
$(document).bind('keydown', 'alt+r', showDialog);
function groupByDate(result) {
const groupedByDate = new Map();
const dayCache = {};
return {
showDialog
};
})();
for (const row of result) {
let dateDay = utils.parseDate(row.dateModifiedTo);
dateDay.setHours(0);
dateDay.setMinutes(0);
dateDay.setSeconds(0);
dateDay.setMilliseconds(0);
// this stupidity is to make sure that we always use the same day object because Map uses only
// reference equality
if (dayCache[dateDay]) {
dateDay = dayCache[dateDay];
}
else {
dayCache[dateDay] = dateDay;
}
if (!groupedByDate.has(dateDay)) {
groupedByDate.set(dateDay, []);
}
groupedByDate.get(dateDay).push(row);
}
return groupedByDate;
}
export default {
showDialog
};

View File

@@ -1,102 +1,113 @@
"use strict";
import treeService from '../services/tree.js';
import messagingService from '../services/messaging.js';
import server from '../services/server.js';
import utils from "../services/utils.js";
import treeUtils from "../services/tree_utils.js";
const recentNotes = (function() {
const $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
const $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
// list of recent note paths
let list = [];
// list of recent note paths
let list = [];
async function reload() {
const result = await server.get('recent-notes');
async function reload() {
const result = await server.get('recent-notes');
list = result.map(r => r.notePath);
list = result.map(r => r.notePath);
}
function addRecentNote(branchId, notePath) {
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === treeService.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
list = result.map(r => r.notePath);
}
}, 1500);
}
async function getNoteTitle(notePath) {
let noteTitle;
try {
noteTitle = await treeUtils.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
messagingService.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
function addRecentNote(noteTreeId, notePath) {
setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === noteTree.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + noteTreeId + '/' + encodeURIComponent(notePath));
return noteTitle;
}
list = result.map(r => r.notePath);
}
}, 1500);
}
async function showDialog() {
glob.activeDialog = $dialog;
function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: 800,
height: 100,
position: { my: "center top+100", at: "top", of: window }
});
$searchInput.val('');
// remove the current note
const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
$searchInput.autocomplete({
source: recNotes.map(notePath => {
let noteTitle;
try {
noteTitle = noteTree.getNotePathTitle(notePath);
}
catch (e) {
noteTitle = "[error - can't find note title]";
messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
}
return {
label: noteTitle,
value: notePath
}
}),
minLength: 0,
autoFocus: true,
select: function (event, ui) {
noteTree.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
});
}
reload();
$(document).bind('keydown', 'ctrl+e', e => {
showDialog();
e.preventDefault();
$dialog.dialog({
modal: true,
width: 800,
height: 100,
position: { my: "center top+100", at: "top", of: window }
});
return {
showDialog,
addRecentNote,
reload
};
})();
$searchInput.val('');
// remove the current note
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
const items = [];
for (const notePath of recNotes) {
items.push({
label: await getNoteTitle(notePath),
value: notePath
});
}
$searchInput.autocomplete({
source: items,
minLength: 0,
autoFocus: true,
select: function (event, ui) {
treeService.activateNode(ui.item.value);
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
},
focus: function (event, ui) {
event.preventDefault();
},
close: function (event, ui) {
if (event.keyCode === 27) { // escape closes dialog
$searchInput.autocomplete('destroy');
$dialog.dialog('close');
}
else {
// keep autocomplete open
// we're kind of abusing autocomplete to work in a way which it's not designed for
$searchInput.autocomplete("search", "");
}
},
create: () => $searchInput.autocomplete("search", ""),
classes: {
"ui-autocomplete": "recent-notes-autocomplete"
}
});
}
setTimeout(reload, 100);
messagingService.subscribeToMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
console.log(utils.now(), "Reloading recent notes because of background changes");
reload();
}
});
export default {
showDialog,
addRecentNote,
reload
};

View File

@@ -1,91 +1,105 @@
"use strict";
import utils from '../services/utils.js';
import libraryLoader from '../services/library_loader.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
const sqlConsole = (function() {
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
const $dialog = $("#sql-console-dialog");
const $query = $('#sql-console-query');
const $executeButton = $('#sql-console-execute');
const $resultHead = $('#sql-console-results thead');
const $resultBody = $('#sql-console-results tbody');
let codeEditor;
let codeEditor;
function showDialog() {
glob.activeDialog = $dialog;
function showDialog() {
glob.activeDialog = $dialog;
$dialog.dialog({
modal: true,
width: $(window).width(),
height: $(window).height(),
open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
$dialog.dialog({
modal: true,
width: $(window).width(),
height: $(window).height(),
open: function() {
initEditor();
}
});
}
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
async function initEditor() {
if (!codeEditor) {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
codeEditor.focus();
}
});
}
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
async function execute() {
const sqlQuery = codeEditor.getValue();
const result = await server.post("sql/execute", {
query: sqlQuery
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
if (!result.success) {
showError(result.error);
return;
}
else {
showMessage("Query was executed successfully.");
}
const rows = result.rows;
$resultHead.empty();
$resultBody.empty();
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
for (const result of rows) {
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
}
$resultBody.append(rowEl);
}
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
}
$(document).bind('keydown', 'alt+o', showDialog);
codeEditor.focus();
}
$query.bind('keydown', 'ctrl+return', execute);
async function execute(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
$executeButton.click(execute);
const sqlQuery = codeEditor.getValue();
return {
showDialog
};
})();
const result = await server.post("sql/execute", {
query: sqlQuery
});
if (!result.success) {
infoService.showError(result.error);
return;
}
else {
infoService.showMessage("Query was executed successfully.");
}
const rows = result.rows;
$resultHead.empty();
$resultBody.empty();
if (rows.length > 0) {
const result = rows[0];
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<th>").html(key));
}
$resultHead.append(rowEl);
}
for (const result of rows) {
const rowEl = $("<tr>");
for (const key in result) {
rowEl.append($("<td>").html(result[key]));
}
$resultBody.append(rowEl);
}
}
$query.bind('keydown', 'ctrl+return', execute);
$executeButton.click(execute);
export default {
showDialog
};

View File

@@ -0,0 +1,26 @@
class Branch {
constructor(treeCache, row) {
this.treeCache = treeCache;
this.branchId = row.branchId;
this.noteId = row.noteId;
this.note = null;
this.parentNoteId = row.parentNoteId;
this.notePosition = row.notePosition;
this.prefix = row.prefix;
this.isExpanded = row.isExpanded;
}
async getNote() {
return await this.treeCache.getNote(this.noteId);
}
isTopLevel() {
return this.parentNoteId === 'root';
}
get toString() {
return `Branch(branchId=${this.branchId})`;
}
}
export default Branch;

View File

@@ -0,0 +1,18 @@
import NoteShort from './note_short.js';
class NoteFull extends NoteShort {
constructor(treeCache, row) {
super(treeCache, row);
this.content = row.content;
if (this.content !== "" && this.isJson()) {
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
}
}
export default NoteFull;

View File

@@ -0,0 +1,61 @@
class NoteShort {
constructor(treeCache, row) {
this.treeCache = treeCache;
this.noteId = row.noteId;
this.title = row.title;
this.isProtected = row.isProtected;
this.type = row.type;
this.mime = row.mime;
this.hideInAutocomplete = row.hideInAutocomplete;
}
isJson() {
return this.mime === "application/json";
}
async getBranches() {
const branches = [];
for (const parent of this.treeCache.parents[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
}
return branches;
}
async getChildBranches() {
if (!this.treeCache.children[this.noteId]) {
return [];
}
const branches = [];
for (const child of this.treeCache.children[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
}
return branches;
}
async getParentNotes() {
return this.treeCache.parents[this.noteId] || [];
}
async getChildNotes() {
return this.treeCache.children[this.noteId] || [];
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
return dto;
}
}
export default NoteShort;

View File

@@ -1,216 +0,0 @@
"use strict";
// hot keys are active also inside inputs and content editables
jQuery.hotkeys.options.filterInputAcceptingElements = false;
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
$(document).bind('keydown', 'alt+m', e => {
$(".hide-toggle").toggleClass("suppressed");
e.preventDefault();
});
// hide (toggle) everything except for the note content for distraction free writing
$(document).bind('keydown', 'alt+t', e => {
const date = new Date();
const dateString = formatDateTime(date);
link.addTextToEditor(dateString);
e.preventDefault();
});
$(document).bind('keydown', 'f5', () => {
reloadApp();
return false;
});
$(document).bind('keydown', 'ctrl+r', () => {
reloadApp();
return false;
});
$(document).bind('keydown', 'ctrl+shift+i', () => {
if (isElectron()) {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
}
});
$(document).bind('keydown', 'ctrl+f', () => {
if (isElectron()) {
const searchInPage = require('electron-in-page-search').default;
const remote = require('electron').remote;
const inPageSearch = searchInPage(remote.getCurrentWebContents());
inPageSearch.openSearchWindow();
return false;
}
});
$(document).bind('keydown', "ctrl+shift+up", () => {
const node = noteTree.getCurrentNode();
node.navigate($.ui.keyCode.UP, true);
$("#note-detail").focus();
return false;
});
$(document).bind('keydown', "ctrl+shift+down", () => {
const node = noteTree.getCurrentNode();
node.navigate($.ui.keyCode.DOWN, true);
$("#note-detail").focus();
return false;
});
$(document).bind('keydown', 'ctrl+-', () => {
if (isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
}
return false;
}
});
$(document).bind('keydown', 'ctrl+=', () => {
if (isElectron()) {
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false;
}
});
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
$(window).on('beforeunload', () => {
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
// this sends the request asynchronously and doesn't wait for result
noteEditor.saveNoteIfChanged();
});
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
$.ui.autocomplete.filter = (array, terms) => {
if (!terms) {
return array;
}
const startDate = new Date();
const results = [];
const tokens = terms.toLowerCase().split(" ");
for (const item of array) {
let found = true;
const lcLabel = item.label.toLowerCase();
for (const token of tokens) {
if (lcLabel.indexOf(token) === -1) {
found = false;
break;
}
}
if (found) {
results.push(item);
if (results.length > 100) {
break;
}
}
}
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
return results;
};
$(document).tooltip({
items: "#note-detail a",
content: function(callback) {
const notePath = link.getNotePathFromLink($(this).attr("href"));
if (notePath !== null) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteEditor.loadNote(noteId).then(note => callback(note.detail.content));
}
},
close: function(event, ui)
{
ui.tooltip.hover(function()
{
$(this).stop(true).fadeTo(400, 1);
},
function()
{
$(this).fadeOut('400', function()
{
$(this).remove();
});
});
}
});
window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase();
let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){
message += 'No details available';
}
else {
message += [
'Message: ' + msg,
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
}
messaging.logError(message);
return false;
};
$("#logout-button").toggle(!isElectron());
$(document).ready(() => {
server.get("script/startup").then(scripts => {
for (const script of scripts) {
executeScript(script);
}
});
});
if (isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
if (!noteTree.noteExists(parentNoteId)) {
await noteTree.reload();
}
await noteTree.activateNode(parentNoteId);
setTimeout(() => {
const node = noteTree.getCurrentNode();
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}

View File

@@ -1,103 +0,0 @@
"use strict";
const link = (function() {
function getNotePathFromLink(url) {
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
if (notePathMatch === null) {
return null;
}
else {
return notePathMatch[1];
}
}
function getNodePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) {
return notePathMatch[1];
}
return null;
}
function createNoteLink(notePath, noteTitle) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = noteTree.getNoteTitle(noteId);
}
const noteLink = $("<a>", {
href: 'javascript:',
text: noteTitle
}).attr('action', 'note')
.attr('note-path', notePath);
return noteLink;
}
function goToLink(e) {
e.preventDefault();
const linkEl = $(e.target);
let notePath = linkEl.attr("note-path");
if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
if (!address) {
return;
}
if (address.startsWith('http')) {
window.open(address, '_blank');
return;
}
notePath = getNotePathFromLink(address);
}
noteTree.activateNode(notePath);
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise
$("[role='tooltip']").remove();
if (glob.activeDialog) {
try {
glob.activeDialog.dialog('close');
}
catch (e) {}
}
}
function addLinkToEditor(linkTitle, linkHref) {
const editor = noteEditor.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
}
function addTextToEditor(text) {
const editor = noteEditor.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
}
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
$(document).on('click', "a[action='note']", goToLink);
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail a', goToLink);
return {
getNodePathFromLabel,
getNotePathFromLink,
createNoteLink,
addLinkToEditor,
addTextToEditor
};
})();

View File

@@ -1,115 +0,0 @@
"use strict";
const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count");
function logError(message) {
console.log(now(), message); // needs to be separate from .trace()
console.trace();
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
type: 'log-error',
error: message
}));
}
}
function messageHandler(event) {
const message = JSON.parse(event.data);
if (message.type === 'sync') {
lastPingTs = new Date().getTime();
if (message.data.length > 0) {
console.log(now(), "Sync data: ", message.data);
lastSyncId = message.data[message.data.length - 1].id;
}
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
if (syncData.some(sync => sync.entityName === 'note_tree')
|| syncData.some(sync => sync.entityName === 'notes')) {
console.log(now(), "Reloading tree because of background changes");
noteTree.reload();
}
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
showMessage('Reloading note because of background changes');
noteEditor.reload();
}
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
console.log(now(), "Reloading recent notes because of background changes");
recentNotes.reload();
}
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000);
}
else if (message.type === 'consistency-checks-failed') {
showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = event => console.log(now(), "Connected to server with WebSocket");
ws.onmessage = messageHandler;
ws.onclose = function(){
// Try to reconnect in 5 seconds
setTimeout(() => connectWebSocket(), 5000);
};
return ws;
}
const ws = connectWebSocket();
let lastSyncId = glob.maxSyncIdAtLoad;
let lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 5000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options
message: "Lost connection to server"
},{
// settings
type: 'danger',
delay: 100000000 // keep it until we explicitly close it
});
}
}
else if (connectionBrokenNotification) {
await connectionBrokenNotification.close();
connectionBrokenNotification = null;
showMessage("Re-connected to server");
}
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}, 1000);
return {
logError
};
})();

View File

@@ -1,20 +1,19 @@
"use strict";
import server from './services/server.js';
$(document).ready(() => {
server.get('migration').then(result => {
const appDbVersion = result.app_db_version;
const dbVersion = result.db_version;
$(document).ready(async () => {
const {appDbVersion, dbVersion} = await server.get('migration');
if (appDbVersion === dbVersion) {
$("#up-to-date").show();
}
else {
$("#need-to-migrate").show();
console.log("HI", {appDbVersion, dbVersion});
$("#app-db-version").html(appDbVersion);
$("#db-version").html(dbVersion);
}
});
if (appDbVersion === dbVersion) {
$("#up-to-date").show();
}
else {
$("#need-to-migrate").show();
$("#app-db-version").html(appDbVersion);
$("#db-version").html(dbVersion);
}
});
$("#run-migration").click(async () => {
@@ -26,7 +25,7 @@ $("#run-migration").click(async () => {
for (const migration of result.migrations) {
const row = $('<tr>')
.append($('<td>').html(migration.db_version))
.append($('<td>').html(migration.dbVersion))
.append($('<td>').html(migration.name))
.append($('<td>').html(migration.success ? 'Yes' : 'No'))
.append($('<td>').html(migration.success ? 'N/A' : migration.error));
@@ -37,4 +36,11 @@ $("#run-migration").click(async () => {
$("#migration-table").append(row);
}
});
// copy of this shortcut to be able to debug migration problems
$(document).bind('keydown', 'ctrl+shift+i', () => {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
});

View File

@@ -1,323 +0,0 @@
"use strict";
const noteEditor = (function() {
const noteTitleEl = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code');
const noteDetailRenderEl = $('#note-detail-render');
const protectButton = $("#protect-button");
const unprotectButton = $("#unprotect-button");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display");
const attributeListEl = $("#attribute-list");
const attributeListInnerEl = $("#attribute-list-inner");
let editor = null;
let codeEditor = null;
let currentNote = null;
let noteChangeDisabled = false;
let isNoteChanged = false;
function getCurrentNote() {
return currentNote;
}
function getCurrentNoteId() {
return currentNote ? currentNote.detail.noteId : null;
}
function noteChanged() {
if (noteChangeDisabled) {
return;
}
isNoteChanged = true;
}
async function reload() {
// no saving here
await loadNoteToEditor(getCurrentNoteId());
}
async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged();
await loadNoteToEditor(noteId);
}
}
async function saveNoteIfChanged() {
if (!isNoteChanged) {
return;
}
const note = noteEditor.getCurrentNote();
updateNoteFromInputs(note);
await saveNoteToServer(note);
if (note.detail.isProtected) {
protected_session.touchProtectedSession();
}
}
function updateNoteFromInputs(note) {
if (note.detail.type === 'text') {
note.detail.content = editor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(note.detail.content).text().trim() === '') {
note.detail.content = ''
}
}
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'render') {
// nothing
}
else {
throwError("Unrecognized type: " + note.detail.type);
}
const title = noteTitleEl.val();
note.detail.title = title;
noteTree.setNoteTitle(note.detail.noteId, title);
}
async function saveNoteToServer(note) {
await server.put('notes/' + note.detail.noteId, note);
isNoteChanged = false;
showMessage("Saved!");
}
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
function newNoteCreated() {
isNewNoteCreated = true;
}
function setContent(content) {
if (currentNote.detail.type === 'text') {
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
}
}
async function loadNoteToEditor(noteId) {
currentNote = await loadNote(noteId);
if (isNewNoteCreated) {
isNewNoteCreated = false;
noteTitleEl.focus().select();
}
noteIdDisplayEl.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
if (currentNote.detail.isProtected) {
protected_session.touchProtectedSession();
}
// this might be important if we focused on protected note when not in protected note and we got a dialog
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show();
noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
if (currentNote.detail.type === 'render') {
noteDetailEl.hide();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree);
}
else {
setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
setNoteBackgroundIfProtected(currentNote);
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
loadAttributeList();
}
async function loadAttributeList() {
const noteId = getCurrentNoteId();
const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html('');
if (attributes.length > 0) {
for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " ");
}
attributeListEl.show();
}
else {
attributeListEl.hide();
}
}
async function loadNote(noteId) {
return await server.get('notes/' + noteId);
}
function getEditor() {
return editor;
}
function focus() {
const note = getCurrentNote();
if (note.detail.type === 'text') {
noteDetailEl.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render') {
// do nothing
}
else {
throwError('Unrecognized type: ' + note.detail.type);
}
}
function getCurrentNoteType() {
const currentNote = getCurrentNote();
return currentNote ? currentNote.detail.type : null;
}
async function executeCurrentNote() {
if (getCurrentNoteType() === 'code') {
// make sure note is saved so we load latest changes
await saveNoteIfChanged();
const script = await server.get('script/subtree/' + getCurrentNoteId());
executeScript(script);
}
}
$(document).ready(() => {
noteTitleEl.on('input', () => {
noteChanged();
const title = noteTitleEl.val();
noteTree.setNoteTitle(getCurrentNoteId(), title);
});
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2);
});
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
setInterval(saveNoteIfChanged, 5000);
return {
reload,
switchToNote,
saveNoteIfChanged,
updateNoteFromInputs,
saveNoteToServer,
setNoteBackgroundIfProtected,
loadNote,
getCurrentNote,
getCurrentNoteType,
getCurrentNoteId,
newNoteCreated,
getEditor,
focus,
executeCurrentNote,
loadAttributeList,
setContent
};
})();

View File

@@ -1,899 +0,0 @@
"use strict";
const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-inner");
let startNotePath = null;
let notesTreeMap = {};
let parentToChildren = {};
let childToParents = {};
let parentChildToNoteTreeId = {};
let noteIdToTitle = {};
let hiddenInAutocomplete = {};
function getNoteTreeId(parentNoteId, childNoteId) {
assertArguments(parentNoteId, childNoteId);
const key = parentNoteId + "-" + childNoteId;
// this can return undefined and client code should deal with it somehow
return parentChildToNoteTreeId[key];
}
function getNoteTitle(noteId, parentNoteId = null) {
assertArguments(noteId);
let title = noteIdToTitle[noteId];
if (!title) {
throwError("Can't find title for noteId='" + noteId + "'");
}
if (parentNoteId !== null) {
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
if (noteTreeId) {
const noteTree = notesTreeMap[noteTreeId];
if (noteTree.prefix) {
title = noteTree.prefix + ' - ' + title;
}
}
}
return title;
}
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() {
return treeEl.fancytree("getActiveNode");
}
function getCurrentNotePath() {
const node = getCurrentNode();
return treeUtils.getNotePath(node);
}
function getNodesByNoteTreeId(noteTreeId) {
assertArguments(noteTreeId);
const noteTree = notesTreeMap[noteTreeId];
return getNodesByNoteId(noteTree.noteId).filter(node => node.data.noteTreeId === noteTreeId);
}
function getNodesByNoteId(noteId) {
assertArguments(noteId);
const list = getTree().getNodesByRef(noteId);
return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
}
function setPrefix(noteTreeId, prefix) {
assertArguments(noteTreeId);
notesTreeMap[noteTreeId].prefix = prefix;
getNodesByNoteTreeId(noteTreeId).map(node => {
node.data.prefix = prefix;
treeUtils.setNodeTitleWithPrefix(node);
});
}
function removeParentChildRelation(parentNoteId, childNoteId) {
assertArguments(parentNoteId, childNoteId);
const key = parentNoteId + "-" + childNoteId;
delete parentChildToNoteTreeId[key];
parentToChildren[parentNoteId] = parentToChildren[parentNoteId].filter(noteId => noteId !== childNoteId);
childToParents[childNoteId] = childToParents[childNoteId].filter(noteId => noteId !== parentNoteId);
}
function setParentChildRelation(noteTreeId, parentNoteId, childNoteId) {
assertArguments(noteTreeId, parentNoteId, childNoteId);
const key = parentNoteId + "-" + childNoteId;
parentChildToNoteTreeId[key] = noteTreeId;
if (!parentToChildren[parentNoteId]) {
parentToChildren[parentNoteId] = [];
}
parentToChildren[parentNoteId].push(childNoteId);
if (!childToParents[childNoteId]) {
childToParents[childNoteId] = [];
}
childToParents[childNoteId].push(parentNoteId);
}
function prepareNoteTree(notes) {
assertArguments(notes);
parentToChildren = {};
childToParents = {};
notesTreeMap = {};
for (const note of notes) {
notesTreeMap[note.noteTreeId] = note;
noteIdToTitle[note.noteId] = note.title;
delete note.title; // this should not be used. Use noteIdToTitle instead
setParentChildRelation(note.noteTreeId, note.parentNoteId, note.noteId);
}
return prepareNoteTreeInner('root');
}
function getExtraClasses(note) {
assertArguments(note);
const extraClasses = [];
if (note.isProtected) {
extraClasses.push("protected");
}
if (childToParents[note.noteId].length > 1) {
extraClasses.push("multiple-parents");
}
if (note.type === 'code') {
extraClasses.push("code");
}
return extraClasses.join(" ");
}
function prepareNoteTreeInner(parentNoteId) {
assertArguments(parentNoteId);
const childNoteIds = parentToChildren[parentNoteId];
if (!childNoteIds) {
messaging.logError("No children for " + parentNoteId + ". This shouldn't happen.");
return;
}
const noteList = [];
for (const noteId of childNoteIds) {
const noteTreeId = getNoteTreeId(parentNoteId, noteId);
const noteTree = notesTreeMap[noteTreeId];
const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.noteId];
const node = {
noteId: noteTree.noteId,
parentNoteId: noteTree.parentNoteId,
noteTreeId: noteTree.noteTreeId,
isProtected: noteTree.isProtected,
prefix: noteTree.prefix,
title: escapeHtml(title),
extraClasses: getExtraClasses(noteTree),
refKey: noteTree.noteId,
expanded: noteTree.isExpanded
};
if (parentToChildren[noteId] && parentToChildren[noteId].length > 0) {
node.folder = true;
if (node.expanded) {
node.children = prepareNoteTreeInner(noteId);
}
else {
node.lazy = true;
}
}
noteList.push(node);
}
return noteList;
}
async function expandToNote(notePath, expandOpts) {
assertArguments(notePath);
const runPath = getRunPath(notePath);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
let parentNoteId = 'root';
for (const childNoteId of runPath) {
const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
if (childNoteId === noteId) {
return node;
}
else {
await node.setExpanded(true, expandOpts);
}
parentNoteId = childNoteId;
}
}
async function activateNode(notePath) {
assertArguments(notePath);
const node = await expandToNote(notePath);
await node.setActive();
clearSelectedNodes();
}
/**
* Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes
* path change) or other corruption, in that case this will try to get some other valid path to the correct note.
*/
function getRunPath(notePath) {
assertArguments(notePath);
const path = notePath.split("/").reverse();
path.push('root');
const effectivePath = [];
let childNoteId = null;
let i = 0;
while (true) {
if (i >= path.length) {
break;
}
const parentNoteId = path[i++];
if (childNoteId !== null) {
const parents = childToParents[childNoteId];
if (!parents) {
messaging.logError("No parents found for " + childNoteId);
return;
}
if (!parents.includes(parentNoteId)) {
console.log(now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
if (parents.length > 0) {
console.log(now(), "Available parents:", parents);
const someNotePath = getSomeNotePath(parents[0]);
if (someNotePath) { // in case it's root the path may be empty
const pathToRoot = someNotePath.split("/").reverse();
for (const noteId of pathToRoot) {
effectivePath.push(noteId);
}
}
break;
}
else {
messaging.logError("No parents, can't activate node.");
return;
}
}
}
if (parentNoteId === 'root') {
break;
}
else {
effectivePath.push(parentNoteId);
childNoteId = parentNoteId;
}
}
return effectivePath.reverse();
}
function showParentList(noteId, node) {
assertArguments(noteId, node);
const parents = childToParents[noteId];
if (!parents) {
throwError("Can't find parents for noteId=" + noteId);
}
if (parents.length <= 1) {
parentListEl.hide();
}
else {
parentListEl.show();
parentListListEl.empty();
for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
const title = getNotePathTitle(notePath);
let item;
if (node.getParent().data.noteId === parentNoteId) {
item = $("<span/>").attr("title", "Current note").append(title);
}
else {
item = link.createNoteLink(notePath, title);
}
parentListListEl.append($("<li/>").append(item));
}
}
}
function getNotePathTitle(notePath) {
assertArguments(notePath);
const titlePath = [];
let parentNoteId = 'root';
for (const noteId of notePath.split('/')) {
titlePath.push(getNoteTitle(noteId, parentNoteId));
parentNoteId = noteId;
}
return titlePath.join(' / ');
}
function getSomeNotePath(noteId) {
assertArguments(noteId);
const path = [];
let cur = noteId;
while (cur !== 'root') {
path.push(cur);
if (!childToParents[cur]) {
throwError("Can't find parents for " + cur);
}
cur = childToParents[cur][0];
}
return path.reverse().join('/');
}
async function setExpandedToServer(noteTreeId, isExpanded) {
assertArguments(noteTreeId);
const expandedNum = isExpanded ? 1 : 0;
await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum);
}
function setCurrentNotePathToHash(node) {
assertArguments(node);
const currentNotePath = treeUtils.getNotePath(node);
const currentNoteTreeId = node.data.noteTreeId;
document.location.hash = currentNotePath;
recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
}
function getSelectedNodes(stopOnParents = false) {
return getTree().getSelectedNodes(stopOnParents);
}
function clearSelectedNodes() {
for (const selectedNode of getSelectedNodes()) {
selectedNode.setSelected(false);
}
const currentNode = getCurrentNode();
if (currentNode) {
currentNode.setSelected(true);
}
}
function initFancyTree(noteTree) {
assertArguments(noteTree);
const keybindings = {
"del": node => {
treeChanges.deleteNodes(getSelectedNodes(true));
},
"ctrl+up": node => {
const beforeNode = node.getPrevSibling();
if (beforeNode !== null) {
treeChanges.moveBeforeNode([node], beforeNode);
}
return false;
},
"ctrl+down": node => {
let afterNode = node.getNextSibling();
if (afterNode !== null) {
treeChanges.moveAfterNode([node], afterNode);
}
return false;
},
"ctrl+left": node => {
treeChanges.moveNodeUpInHierarchy(node);
return false;
},
"ctrl+right": node => {
let toNode = node.getPrevSibling();
if (toNode !== null) {
treeChanges.moveToNode([node], toNode);
}
return false;
},
"shift+up": node => {
node.navigate($.ui.keyCode.UP, true).then(() => {
const currentNode = getCurrentNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"shift+down": node => {
node.navigate($.ui.keyCode.DOWN, true).then(() => {
const currentNode = getCurrentNode();
if (currentNode.isSelected()) {
node.setSelected(false);
}
currentNode.setSelected(true);
});
return false;
},
"f2": node => {
editTreePrefix.showDialog(node);
},
"alt+-": node => {
collapseTree(node);
},
"alt+s": node => {
sortAlphabetically(node.data.noteId);
return false;
},
"ctrl+a": node => {
for (const child of node.getParent().getChildren()) {
child.setSelected(true);
}
return false;
},
"ctrl+c": () => {
contextMenu.copy(getSelectedNodes());
return false;
},
"ctrl+x": () => {
contextMenu.cut(getSelectedNodes());
return false;
},
"ctrl+v": node => {
contextMenu.pasteInto(node);
return false;
},
"return": node => {
noteEditor.focus();
return false;
},
"backspace": node => {
if (!isTopLevelNode(node)) {
node.getParent().setActive().then(() => clearSelectedNodes());
}
},
// code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
// after opening context menu, standard shortcuts don't work, but they are detected here
// so we essentially takeover the standard handling with our implementation.
"left": node => {
node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
return false;
},
"right": node => {
node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
return false;
},
"up": node => {
node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
return false;
},
"down": node => {
node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
return false;
}
};
treeEl.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: noteTree,
scrollParent: $("#tree"),
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;
if (targetType === 'title' || targetType === 'icon') {
if (!event.ctrlKey) {
node.setActive();
node.setSelected(true);
clearSelectedNodes();
}
else {
node.setSelected(!node.isSelected());
}
return false;
}
},
activate: (event, data) => {
const node = data.node.data;
setCurrentNotePathToHash(data.node);
noteEditor.switchToNote(node.noteId);
showParentList(node.noteId, data.node);
},
expand: (event, data) => {
setExpandedToServer(data.node.data.noteTreeId, true);
},
collapse: (event, data) => {
setExpandedToServer(data.node.data.noteTreeId, false);
},
init: (event, data) => {
const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
if (noteIdToTitle[noteId] === undefined) {
// note doesn't exist so don't try to activate it
startNotePath = null;
}
if (startNotePath) {
activateNode(startNotePath);
// looks like this this doesn't work when triggered immediatelly after activating node
// so waiting a second helps
setTimeout(scrollToCurrentNote, 1000);
}
},
hotkeys: {
keydown: keybindings
},
filter: {
autoApply: true, // Re-apply last filter if lazy data is loaded
autoExpand: true, // Expand all branches that contain matches while filtered
counter: false, // Show a badge with number of matching child nodes near parent icons
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
hideExpandedCounter: true, // Hide counter badge if parent is expanded
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
highlight: true, // Highlight matches by wrapping inside <mark> tags
leavesOnly: false, // Match end nodes only
nodata: true, // Display a 'no data' status node if result is empty
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
},
dnd: dragAndDropSetup,
lazyLoad: function(event, data){
const node = data.node.data;
data.result = prepareNoteTreeInner(node.noteId);
},
clones: {
highlightActiveClones: true
}
});
treeEl.contextmenu(contextMenu.contextMenuSettings);
}
function getTree() {
return treeEl.fancytree('getTree');
}
async function reload() {
const notes = await loadTree();
// this will also reload the note content
await getTree().reload(notes);
}
function getNotePathFromAddress() {
return document.location.hash.substr(1); // strip initial #
}
async function loadTree() {
const resp = await server.get('tree');
startNotePath = resp.start_note_path;
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
}
hiddenInAutocomplete = {};
for (const noteId of resp.hiddenInAutocomplete) {
hiddenInAutocomplete[noteId] = true;
}
return prepareNoteTree(resp.notes);
}
$(() => loadTree().then(noteTree => initFancyTree(noteTree)));
function collapseTree(node = null) {
if (!node) {
node = treeEl.fancytree("getRootNode");
}
node.setExpanded(false);
node.visit(node => node.setExpanded(false));
}
$(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
function scrollToCurrentNote() {
const node = getCurrentNode();
if (node) {
node.makeVisible({scrollIntoView: true});
node.setFocus();
}
}
function setNoteTreeBackgroundBasedOnProtectedStatus(noteId) {
getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
}
function setProtected(noteId, isProtected) {
getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected);
setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
}
function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) {
parentNoteId = 'root';
}
if (!parentToChildren[parentNoteId]) {
return [];
}
if (!notePath) {
notePath = '';
}
if (!titlePath) {
titlePath = '';
}
const autocompleteItems = [];
for (const childNoteId of parentToChildren[parentNoteId]) {
if (hiddenInAutocomplete[childNoteId]) {
continue;
}
const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
autocompleteItems.push({
value: childTitlePath + ' (' + childNotePath + ')',
label: childTitlePath
});
const childItems = getAutocompleteItems(childNoteId, childNotePath, childTitlePath);
for (const childItem of childItems) {
autocompleteItems.push(childItem);
}
}
return autocompleteItems;
}
function setNoteTitle(noteId, title) {
assertArguments(noteId);
noteIdToTitle[noteId] = title;
getNodesByNoteId(noteId).map(clone => treeUtils.setNodeTitleWithPrefix(clone));
}
async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode");
await createNote(rootNode, "root", "into");
}
async function createNote(node, parentNoteId, target, isProtected) {
assertArguments(node, parentNoteId, target);
// if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
// but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
if (!isProtected || !protected_session.isProtectedSessionAvailable()) {
isProtected = false;
}
const newNoteName = "new note";
const result = await server.post('notes/' + parentNoteId + '/children', {
title: newNoteName,
target: target,
target_noteTreeId: node.data.noteTreeId,
isProtected: isProtected
});
setParentChildRelation(result.noteTreeId, parentNoteId, result.noteId);
notesTreeMap[result.noteTreeId] = result;
noteIdToTitle[result.noteId] = newNoteName;
noteEditor.newNoteCreated();
const newNode = {
title: newNoteName,
noteId: result.noteId,
parentNoteId: parentNoteId,
refKey: result.noteId,
noteTreeId: result.noteTreeId,
isProtected: isProtected,
extraClasses: getExtraClasses(result.note)
};
if (target === 'after') {
await node.appendSibling(newNode).setActive(true);
}
else if (target === 'into') {
if (!node.getChildren() && node.isFolder()) {
await node.setExpanded();
}
else {
node.addChildren(newNode);
}
await node.getLastChild().setActive(true);
node.folder = true;
node.renderTitle();
}
else {
throwError("Unrecognized target: " + target);
}
clearSelectedNodes(); // to unmark previously active node
showMessage("Created!");
}
async function sortAlphabetically(noteId) {
await server.put('notes/' + noteId + '/sort');
await reload();
}
function noteExists(noteId) {
return !!childToParents[noteId];
}
$(document).bind('keydown', 'ctrl+o', e => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
createNote(node, parentNoteId, 'after', isProtected);
e.preventDefault();
});
$(document).bind('keydown', 'ctrl+p', e => {
const node = getCurrentNode();
createNote(node, node.data.noteId, 'into', node.data.isProtected);
e.preventDefault();
});
$(document).bind('keydown', 'ctrl+del', e => {
const node = getCurrentNode();
treeChanges.deleteNodes([node]);
e.preventDefault();
});
$(document).bind('keydown', 'ctrl+.', scrollToCurrentNote);
$(window).bind('hashchange', function() {
const notePath = getNotePathFromAddress();
if (getCurrentNotePath() !== notePath) {
console.log("Switching to " + notePath + " because of hash change");
activateNode(notePath);
}
});
if (isElectron()) {
$(document).bind('keydown', 'alt+left', e => {
window.history.back();
e.preventDefault();
});
$(document).bind('keydown', 'alt+right', e => {
window.history.forward();
e.preventDefault();
});
}
return {
reload,
collapseTree,
scrollToCurrentNote,
setNoteTreeBackgroundBasedOnProtectedStatus,
setProtected,
getCurrentNode,
expandToNote,
activateNode,
getCurrentNotePath,
getNoteTitle,
setCurrentNotePathToHash,
getAutocompleteItems,
setNoteTitle,
createNewTopLevelNote,
createNote,
setPrefix,
getNotePathTitle,
removeParentChildRelation,
setParentChildRelation,
getSelectedNodes,
sortAlphabetically,
noteExists
};
})();

View File

@@ -1,134 +0,0 @@
"use strict";
const noteType = (function() {
const executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
const self = this;
this.type = ko.observable('text');
this.mime = ko.observable('');
this.codeMimeTypes = ko.observableArray([
{ mime: 'text/x-csrc', title: 'C' },
{ mime: 'text/x-c++src', title: 'C++' },
{ mime: 'text/x-csharp', title: 'C#' },
{ mime: 'text/x-clojure', title: 'Clojure' },
{ mime: 'text/css', title: 'CSS' },
{ mime: 'text/x-dockerfile', title: 'Dockerfile' },
{ mime: 'text/x-erlang', title: 'Erlang' },
{ mime: 'text/x-feature', title: 'Gherkin' },
{ mime: 'text/x-go', title: 'Go' },
{ mime: 'text/x-groovy', title: 'Groovy' },
{ mime: 'text/x-haskell', title: 'Haskell' },
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript', title: 'JavaScript' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
{ mime: 'text/x-markdown', title: 'Markdown' },
{ mime: 'text/x-objectivec', title: 'Objective C' },
{ mime: 'text/x-pascal', title: 'Pascal' },
{ mime: 'text/x-perl', title: 'Perl' },
{ mime: 'text/x-php', title: 'PHP' },
{ mime: 'text/x-python', title: 'Python' },
{ mime: 'text/x-ruby', title: 'Ruby' },
{ mime: 'text/x-rustsrc', title: 'Rust' },
{ mime: 'text/x-scala', title: 'Scala' },
{ mime: 'text/x-sh', title: 'Shell' },
{ mime: 'text/x-sql', title: 'SQL' },
{ mime: 'text/x-swift', title: 'Swift' },
{ mime: 'text/xml', title: 'XML' },
{ mime: 'text/x-yaml', title: 'YAML' }
]);
this.typeString = function() {
const type = self.type();
const mime = self.mime();
if (type === 'text') {
return 'Text';
}
else if (type === 'code') {
if (!mime) {
return 'Code';
}
else {
const found = self.codeMimeTypes().find(x => x.mime === mime);
return found ? found.title : mime;
}
}
else if (type === 'render') {
return 'Render HTML note';
}
else {
throwError('Unrecognized type: ' + type);
}
};
async function save() {
const note = noteEditor.getCurrentNote();
await server.put('notes/' + note.detail.noteId
+ '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime()));
await noteEditor.reload();
// for the note icon to be updated in the tree
await noteTree.reload();
self.updateExecuteScriptButtonVisibility();
}
this.selectText = function() {
self.type('text');
self.mime('');
save();
};
this.selectRender = function() {
self.type('render');
self.mime('');
save();
};
this.selectCode = function() {
self.type('code');
self.mime('');
save();
};
this.selectCodeMime = function(el) {
self.type('code');
self.mime(el.mime);
save();
};
this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript');
}
}
ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
return {
getNoteType: () => noteTypeModel.type(),
setNoteType: type => noteTypeModel.type(type),
getNoteMime: () => noteTypeModel.mime(),
setNoteMime: mime => {
noteTypeModel.mime(mime);
noteTypeModel.updateExecuteScriptButtonVisibility();
}
};
})();

View File

@@ -1,182 +0,0 @@
"use strict";
const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper");
let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null;
let protectedSessionTimeout = null;
let protectedSessionId = null;
$(document).ready(() => {
server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
});
function setProtectedSessionTimeout(encSessTimeout) {
protectedSessionTimeout = encSessTimeout;
}
function ensureProtectedSession(requireProtectedSession, modal) {
const dfd = $.Deferred();
if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide();
dialogEl.dialog({
modal: modal,
width: 400,
open: () => {
if (!modal) {
// dialog steals focus for itself, which is not what we want for non-modal (viewing)
noteTree.getCurrentNode().setFocus();
}
}
});
}
else {
dfd.resolve();
}
return dfd.promise();
}
async function setupProtectedSession() {
const password = passwordEl.val();
passwordEl.val("");
const response = await enterProtectedSession(password);
if (!response.success) {
showError("Wrong password.");
return;
}
protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close");
noteEditor.reload();
noteTree.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl);
noteDetailWrapperEl.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
}
}
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
dialogEl.dialog('close');
}
catch (e) {}
passwordEl.val('');
}
async function enterProtectedSession(password) {
return await server.post('login/protected', {
password: password
});
}
function getProtectedSessionId() {
return protectedSessionId;
}
function resetProtectedSession() {
protectedSessionId = null;
// most secure solution - guarantees nothing remained in memory
// since this expires because user doesn't use the app, it shouldn't be disruptive
reloadApp();
}
function isProtectedSessionAvailable() {
return protectedSessionId !== null;
}
async function protectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteEditor.getCurrentNote();
noteEditor.updateNoteFromInputs(note);
note.detail.isProtected = true;
await noteEditor.saveNoteToServer(note);
noteTree.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
}
async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteEditor.getCurrentNote();
noteEditor.updateNoteFromInputs(note);
note.detail.isProtected = false;
await noteEditor.saveNoteToServer(note);
noteTree.setProtected(note.detail.noteId, note.detail.isProtected);
noteEditor.setNoteBackgroundIfProtected(note);
}
function touchProtectedSession() {
if (isProtectedSessionAvailable()) {
lastProtectedSessionOperationDate = new Date();
}
}
async function protectSubTree(noteId, protect) {
await ensureProtectedSession(true, true);
await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
showMessage("Request to un/protect sub tree has finished successfully");
noteTree.reload();
noteEditor.reload();
}
passwordFormEl.submit(() => {
setupProtectedSession();
return false;
});
setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
resetProtectedSession();
}
}, 5000);
return {
setProtectedSessionTimeout,
ensureProtectedSession,
resetProtectedSession,
isProtectedSessionAvailable,
protectNoteAndSendToServer,
unprotectNoteAndSendToServer,
getProtectedSessionId,
touchProtectedSession,
protectSubTree,
ensureDialogIsClosed
};
})();

View File

@@ -1,62 +0,0 @@
"use strict";
const searchTree = (function() {
const treeEl = $("#tree");
const searchInputEl = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box");
resetSearchButton.click(resetSearch);
function toggleSearch() {
if (searchBoxEl.is(":hidden")) {
searchBoxEl.show();
searchInputEl.focus();
}
else {
resetSearch();
searchBoxEl.hide();
}
}
function resetSearch() {
searchInputEl.val("");
getTree().clearFilter();
}
function getTree() {
return treeEl.fancytree('getTree');
}
searchInputEl.keyup(async e => {
const searchText = searchInputEl.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click();
return;
}
if (e && e.which === $.ui.keyCode.ENTER) {
const noteIds = await server.get('notes?search=' + encodeURIComponent(searchText));
for (const noteId of noteIds) {
await noteTree.expandToNote(noteId, {noAnimation: true, noEvents: true});
}
// Pass a string to perform case insensitive matching
getTree().filterBranches(node => noteIds.includes(node.data.noteId));
}
}).focus();
$(document).bind('keydown', 'ctrl+s', e => {
toggleSearch();
e.preventDefault();
});
return {
toggleSearch
};
})();

View File

@@ -1,111 +0,0 @@
const server = (function() {
function getHeaders() {
let protectedSessionId = null;
try { // this is because protected session might not be declared in some cases - like when it's included in migration page
protectedSessionId = protected_session.getProtectedSessionId();
}
catch(e) {}
// headers need to be lowercase because node.js automatically converts them to lower case
// so hypothetical protectedSessionId becomes protectedsessionid on the backend
return {
protected_session_id: protectedSessionId,
source_id: glob.sourceId
};
}
async function get(url) {
return await call('GET', url);
}
async function post(url, data) {
return await call('POST', url, data);
}
async function put(url, data) {
return await call('PUT', url, data);
}
async function remove(url) {
return await call('DELETE', url);
}
async function exec(params, script) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await post('script/exec', { script: script, params: params });
return ret.executionResult;
}
let i = 1;
const reqResolves = {};
async function call(method, url, data) {
if (isElectron()) {
const ipc = require('electron').ipcRenderer;
const requestId = i++;
return new Promise((resolve, reject) => {
reqResolves[requestId] = resolve;
console.log(now(), "Request #" + requestId + " to " + method + " " + url);
ipc.send('server-request', {
requestId: requestId,
headers: getHeaders(),
method: method,
url: "/" + baseApiUrl + url,
data: data
});
});
}
else {
return await ajax(url, method, data);
}
}
if (isElectron()) {
const ipc = require('electron').ipcRenderer;
ipc.on('server-response', (event, arg) => {
console.log(now(), "Response #" + arg.requestId + ": " + arg.statusCode);
reqResolves[arg.requestId](arg.body);
delete reqResolves[arg.requestId];
});
}
async function ajax(url, method, data) {
const options = {
url: baseApiUrl + url,
type: method,
headers: getHeaders()
};
if (data) {
options.data = JSON.stringify(data);
options.contentType = "application/json";
}
return await $.ajax(options).catch(e => {
const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
showError(message);
throwError(message);
});
}
return {
get,
post,
put,
remove,
exec,
// don't remove, used from CKEditor image upload!
getHeaders
}
})();

View File

@@ -0,0 +1,104 @@
import treeCache from "./tree_cache.js";
import treeUtils from "./tree_utils.js";
import protectedSessionHolder from './protected_session_holder.js';
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
if (!parentNoteId) {
parentNoteId = 'root';
}
const parentNote = await treeCache.getNote(parentNoteId);
const childNotes = await parentNote.getChildNotes();
if (!childNotes.length) {
return [];
}
if (!notePath) {
notePath = '';
}
if (!titlePath) {
titlePath = '';
}
const autocompleteItems = [];
for (const childNote of childNotes) {
if (childNote.hideInAutocomplete) {
continue;
}
const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
autocompleteItems.push({
value: childTitlePath + ' (' + childNotePath + ')',
label: childTitlePath
});
}
const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
for (const childItem of childItems) {
autocompleteItems.push(childItem);
}
}
if (parentNoteId === 'root') {
console.log(`Generated ${autocompleteItems.length} autocomplete items`);
}
return autocompleteItems;
}
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
$.ui.autocomplete.filter = (array, terms) => {
if (!terms) {
return array;
}
const startDate = new Date();
const results = [];
const tokens = terms.toLowerCase().split(" ");
for (const item of array) {
const lcLabel = item.label.toLowerCase();
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (!found) {
continue;
}
// this is not completely correct and might cause minor problems with note with names containing this " / "
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
if (lastSegmentIndex !== -1) {
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
// at least some token needs to be in the last segment (leaf note), otherwise this
// particular note is not that interesting (query is satisfied by parent note)
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
if (!foundInLastSegment) {
continue;
}
}
results.push(item);
if (results.length > 100) {
break;
}
}
console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
return results;
};
export default {
getAutocompleteItems
};

View File

@@ -0,0 +1,92 @@
import addLinkDialog from '../dialogs/add_link.js';
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
import labelsDialog from '../dialogs/labels.js';
import noteRevisionsDialog from '../dialogs/note_revisions.js';
import noteSourceDialog from '../dialogs/note_source.js';
import recentChangesDialog from '../dialogs/recent_changes.js';
import recentNotesDialog from '../dialogs/recent_notes.js';
import optionsDialog from '../dialogs/options.js';
import sqlConsoleDialog from '../dialogs/sql_console.js';
import cloning from './cloning.js';
import contextMenu from './context_menu.js';
import dragAndDropSetup from './drag_and_drop.js';
import exportService from './export.js';
import link from './link.js';
import messagingService from './messaging.js';
import noteDetailService from './note_detail.js';
import noteType from './note_type.js';
import protected_session from './protected_session.js';
import searchTreeService from './search_tree.js';
import ScriptApi from './script_api.js';
import ScriptContext from './script_context.js';
import sync from './sync.js';
import treeService from './tree.js';
import treeChanges from './branches.js';
import treeUtils from './tree_utils.js';
import utils from './utils.js';
import server from './server.js';
import entrypoints from './entrypoints.js';
import tooltip from './tooltip.js';
import bundle from "./bundle.js";
import treeCache from "./tree_cache.js";
import libraryLoader from "./library_loader.js";
// required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode;
window.glob.getHeaders = server.getHeaders;
// required for ESLint plugin
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
window.glob.requireLibrary = libraryLoader.requireLibrary;
window.glob.ESLINT = libraryLoader.ESLINT;
window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase();
let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){
message += 'No details available';
}
else {
message += [
'Message: ' + msg,
'URL: ' + url,
'Line: ' + lineNo,
'Column: ' + columnNo,
'Error object: ' + JSON.stringify(error)
].join(' - ');
}
messagingService.logError(message);
return false;
};
$("#logout-button").toggle(!utils.isElectron());
if (utils.isElectron()) {
require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
// this might occur when day note had to be created
if (!await treeCache.getNote(parentNoteId)) {
await treeService.reload();
}
await treeService.activateNode(parentNoteId);
setTimeout(() => {
const node = treeService.getCurrentNode();
treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}
treeService.showTree();
entrypoints.registerEntrypoints();
tooltip.setupTooltip();
bundle.executeStartupBundles();

View File

@@ -0,0 +1,135 @@
import treeService from './tree.js';
import utils from './utils.js';
import server from './server.js';
import infoService from "./info.js";
import treeCache from "./tree_cache.js";
async function moveBeforeNode(nodesToMove, beforeNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
await changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
}
}
async function moveAfterNode(nodesToMove, afterNode) {
nodesToMove.reverse(); // need to reverse to keep the note order
for (const nodeToMove of nodesToMove) {
const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
await changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
}
}
async function moveToNode(nodesToMove, toNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
if (!resp.success) {
alert(resp.message);
return;
}
await changeNode(nodeToMove, async node => {
// first expand which will force lazy load and only then move the node
// if this is not expanded before moving, then lazy load won't happen because it already contains node
// this doesn't work if this isn't a folder yet, that's why we expand second time below
await toNode.setExpanded(true);
node.moveTo(toNode);
toNode.folder = true;
toNode.renderTitle();
// this expands the note in case it become the folder only after the move
await toNode.setExpanded(true);
});
}
}
async function deleteNodes(nodes) {
if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
return;
}
for (const node of nodes) {
await server.remove('branches/' + node.data.branchId);
}
// following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
// called with stopOnParent=true
let next = nodes[nodes.length - 1].getNextSibling();
if (!next) {
next = nodes[0].getPrevSibling();
}
if (!next && !utils.isTopLevelNode(nodes[0])) {
next = nodes[0].getParent();
}
if (next) {
// activate next element after this one is deleted so we don't lose focus
next.setActive();
treeService.setCurrentNotePathToHash(next);
}
infoService.showMessage("Note(s) has been deleted.");
await treeService.reload();
}
async function moveNodeUpInHierarchy(node) {
if (utils.isTopLevelNode(node)) {
return;
}
const resp = await server.put('branches/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
if (!resp.success) {
alert(resp.message);
return;
}
if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
node.getParent().folder = false;
node.getParent().renderTitle();
}
await changeNode(node, node => node.moveTo(node.getParent(), 'after'));
}
async function changeNode(node, func) {
utils.assertArguments(node.data.parentNoteId, node.data.noteId);
const childNoteId = node.data.noteId;
const oldParentNoteId = node.data.parentNoteId;
await func(node);
const newParentNoteId = node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
await treeCache.moveNote(childNoteId, oldParentNoteId, newParentNoteId);
treeService.setCurrentNotePathToHash(node);
}
export default {
moveBeforeNode,
moveAfterNode,
moveToNode,
deleteNodes,
moveNodeUpInHierarchy
};

View File

@@ -0,0 +1,23 @@
import ScriptContext from "./script_context.js";
import server from "./server.js";
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
}.call(apiContext));
}
async function executeStartupBundles() {
const scriptBundles = await server.get("script/startup");
for (const bundle of scriptBundles) {
await executeBundle(bundle);
}
}
export default {
executeBundle,
executeStartupBundles
}

View File

@@ -0,0 +1,32 @@
import treeService from './tree.js';
import server from './server.js';
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
prefix: prefix
});
if (!resp.success) {
alert(resp.message);
return;
}
await treeService.reload();
}
// beware that first arg is noteId and second is branchId!
async function cloneNoteAfter(noteId, afterBranchId) {
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId);
if (!resp.success) {
alert(resp.message);
return;
}
await treeService.reload();
}
export default {
cloneNoteAfter,
cloneNoteTo
};

View File

@@ -0,0 +1,189 @@
import treeService from './tree.js';
import cloningService from './cloning.js';
import exportService from './export.js';
import messagingService from './messaging.js';
import protectedSessionService from './protected_session.js';
import treeChangesService from './branches.js';
import treeUtils from './tree_utils.js';
import branchPrefixDialog from '../dialogs/branch_prefix.js';
import infoService from "./info.js";
import treeCache from "./tree_cache.js";
import syncService from "./sync.js";
const $tree = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
async function pasteAfter(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChangesService.moveAfterNode(nodes, node);
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteAfter(noteId, node.data.branchId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
infoService.throwError("Unrecognized clipboard mode=" + clipboardMode);
}
}
async function pasteInto(node) {
if (clipboardMode === 'cut') {
const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
await treeChangesService.moveToNode(nodes, node);
clipboardIds = [];
clipboardMode = null;
}
else if (clipboardMode === 'copy') {
for (const noteId of clipboardIds) {
await cloningService.cloneNoteTo(noteId, node.data.noteId);
}
// copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
}
else if (clipboardIds.length === 0) {
// just do nothing
}
else {
infoService.throwError("Unrecognized clipboard mode=" + mode);
}
}
function copy(nodes) {
clipboardIds = nodes.map(node => node.data.noteId);
clipboardMode = 'copy';
infoService.showMessage("Note(s) have been copied into clipboard.");
}
function cut(nodes) {
clipboardIds = nodes.map(node => node.key);
clipboardMode = 'cut';
infoService.showMessage("Note(s) have been cut into clipboard.");
}
const contextMenuOptions = {
delegate: "span.fancytree-title",
autoFocus: true,
menu: [
{title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
{title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
{title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
{title: "----"},
{title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "ui-icon-pencil"},
{title: "----"},
{title: "Protect branch", cmd: "protectBranch", uiIcon: "ui-icon-locked"},
{title: "Unprotect branch", cmd: "unprotectBranch", uiIcon: "ui-icon-unlocked"},
{title: "----"},
{title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
{title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "----"},
{title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
],
beforeOpen: async (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
const branch = await treeCache.getBranch(node.data.branchId);
const note = await treeCache.getNote(node.data.noteId);
const parentNote = await treeCache.getNote(branch.parentNoteId);
// Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
// Activate node on right-click
node.setActive();
// Disable tree keyboard handling
ui.menu.prevKeyboard = node.tree.options.keyboard;
node.tree.options.keyboard = false;
},
close: (event, ui) => {},
select: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
if (ui.cmd === "insertNoteHere") {
const parentNoteId = node.data.parentNoteId;
const isProtected = treeUtils.getParentProtectedStatus(node);
treeService.createNote(node, parentNoteId, 'after', isProtected);
}
else if (ui.cmd === "insertChildNote") {
treeService.createNote(node, node.data.noteId, 'into');
}
else if (ui.cmd === "editBranchPrefix") {
branchPrefixDialog.showDialog(node);
}
else if (ui.cmd === "protectBranch") {
protectedSessionService.protectBranch(node.data.noteId, true);
}
else if (ui.cmd === "unprotectBranch") {
protectedSessionService.protectBranch(node.data.noteId, false);
}
else if (ui.cmd === "copy") {
copy(treeService.getSelectedNodes());
}
else if (ui.cmd === "cut") {
cut(treeService.getSelectedNodes());
}
else if (ui.cmd === "pasteAfter") {
pasteAfter(node);
}
else if (ui.cmd === "pasteInto") {
pasteInto(node);
}
else if (ui.cmd === "delete") {
treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
}
else if (ui.cmd === "exportBranch") {
exportService.exportBranch(node.data.noteId);
}
else if (ui.cmd === "importBranch") {
exportService.importBranch(node.data.noteId);
}
else if (ui.cmd === "collapseBranch") {
treeService.collapseTree(node);
}
else if (ui.cmd === "forceNoteSync") {
syncService.forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sortAlphabetically") {
treeService.sortAlphabetically(node.data.noteId);
}
else {
messagingService.logError("Unknown command: " + ui.cmd);
}
}
};
export default {
pasteAfter,
pasteInto,
cut,
copy,
contextMenuOptions
};

View File

@@ -1,4 +1,5 @@
"use strict";
import treeService from './tree.js';
import treeChangesService from './branches.js';
const dragAndDropSetup = {
autoExpandMS: 600,
@@ -49,19 +50,21 @@ const dragAndDropSetup = {
const nodeToMove = data.otherNode;
nodeToMove.setSelected(true);
const selectedNodes = noteTree.getSelectedNodes();
const selectedNodes = treeService.getSelectedNodes();
if (data.hitMode === "before") {
treeChanges.moveBeforeNode(selectedNodes, node);
treeChangesService.moveBeforeNode(selectedNodes, node);
}
else if (data.hitMode === "after") {
treeChanges.moveAfterNode(selectedNodes, node);
treeChangesService.moveAfterNode(selectedNodes, node);
}
else if (data.hitMode === "over") {
treeChanges.moveToNode(selectedNodes, node);
treeChangesService.moveToNode(selectedNodes, node);
}
else {
throw new Exception("Unknown hitMode=" + data.hitMode);
}
}
};
export default dragAndDropSetup;

View File

@@ -0,0 +1,133 @@
import utils from "./utils.js";
import treeService from "./tree.js";
import linkService from "./link.js";
import fileService from "./file.js";
import noteRevisionsDialog from "../dialogs/note_revisions.js";
import optionsDialog from "../dialogs/options.js";
import addLinkDialog from "../dialogs/add_link.js";
import recentNotesDialog from "../dialogs/recent_notes.js";
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
import noteSourceDialog from "../dialogs/note_source.js";
import recentChangesDialog from "../dialogs/recent_changes.js";
import sqlConsoleDialog from "../dialogs/sql_console.js";
import searchTreeService from "./search_tree.js";
import labelsDialog from "../dialogs/labels.js";
function registerEntrypoints() {
// hot keys are active also inside inputs and content editables
jQuery.hotkeys.options.filterInputAcceptingElements = false;
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
$("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
$("#show-source-button").click(noteSourceDialog.showDialog);
utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
$("#recent-changes-button").click(recentChangesDialog.showDialog);
$("#recent-notes-button").click(recentNotesDialog.showDialog);
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
$("#toggle-search-button").click(searchTreeService.toggleSearch);
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
$(".show-labels-button").click(labelsDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog);
$("#options-button").click(optionsDialog.showDialog);
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
if (utils.isElectron()) {
utils.bindShortcut('alt+left', window.history.back);
utils.bindShortcut('alt+right', window.history.forward);
}
utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
// hide (toggle) everything except for the note content for distraction free writing
utils.bindShortcut('alt+t', e => {
const date = new Date();
const dateString = utils.formatDateTime(date);
linkService.addTextToEditor(dateString);
});
utils.bindShortcut('f5', utils.reloadApp);
utils.bindShortcut('ctrl+r', utils.reloadApp);
$(document).bind('keydown', 'ctrl+shift+i', () => {
if (utils.isElectron()) {
require('electron').remote.getCurrentWindow().toggleDevTools();
return false;
}
});
$(document).bind('keydown', 'ctrl+f', () => {
if (utils.isElectron()) {
const searchInPage = require('electron-in-page-search').default;
const remote = require('electron').remote;
const inPageSearch = searchInPage(remote.getCurrentWebContents());
inPageSearch.openSearchWindow();
return false;
}
});
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+up", () => {
const node = treeService.getCurrentNode();
node.navigate($.ui.keyCode.UP, true);
$("#note-detail-text").focus();
});
// FIXME: do we really need these at this point?
utils.bindShortcut("ctrl+shift+down", () => {
const node = treeService.getCurrentNode();
node.navigate($.ui.keyCode.DOWN, true);
$("#note-detail-text").focus();
});
$(document).bind('keydown', 'ctrl+-', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
}
return false;
}
});
$(document).bind('keydown', 'ctrl+=', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false;
}
});
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());
$("#upload-file-button").click(fileService.uploadFile);
}
export default {
registerEntrypoints
}

View File

@@ -0,0 +1,40 @@
import treeService from './tree.js';
import protectedSessionHolder from './protected_session_holder.js';
import utils from './utils.js';
import server from './server.js';
function exportBranch(noteId) {
const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId="
+ encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
utils.download(url);
}
let importNoteId;
function importBranch(noteId) {
importNoteId = noteId;
$("#import-upload").trigger('click');
}
$("#import-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
await $.ajax({
url: baseApiUrl + 'notes/' + importNoteId + '/import',
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await treeService.reload();
});
export default {
exportBranch,
importBranch
};

View File

@@ -0,0 +1,29 @@
import noteDetailService from "./note_detail.js";
import treeService from "./tree.js";
import server from "./server.js";
function uploadFile() {
$("#file-upload").trigger('click');
}
$("#file-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'notes/' + noteDetailService.getCurrentNoteId() + '/upload',
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await treeService.reload();
await treeService.activateNode(resp.noteId);
});
export default {
uploadFile
}

View File

@@ -0,0 +1,40 @@
import messagingService from "./messaging.js";
import utils from "./utils.js";
function showMessage(message) {
console.log(utils.now(), "message: ", message);
$.notify({
// options
message: message
}, {
// options
type: 'success',
delay: 3000
});
}
function showError(message, delay = 10000) {
console.log(utils.now(), "error: ", message);
$.notify({
// options
message: message
}, {
// options
type: 'danger',
delay: delay
});
}
function throwError(message) {
messagingService.logError(message);
throw new Error(message);
}
export default {
showMessage,
showError,
throwError
}

View File

@@ -0,0 +1,65 @@
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],
css: [
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
]
};
const ESLINT = {js: ["libraries/eslint.js"]};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
// we save the promises in case of the same script being required concurrently multiple times
const loadedScriptPromises = {};
async function requireScript(url) {
if (!loadedScriptPromises[url]) {
loadedScriptPromises[url] = $.ajax({
url: url,
dataType: "script",
cache: true
});
}
await loadedScriptPromises[url];
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
export default {
requireLibrary,
CKEDITOR,
CODE_MIRROR,
ESLINT
}

View File

@@ -0,0 +1,105 @@
import treeService from './tree.js';
import noteDetailText from './note_detail_text.js';
import treeUtils from './tree_utils.js';
function getNotePathFromLink(url) {
const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
if (notePathMatch === null) {
return null;
}
else {
return notePathMatch[1];
}
}
function getNodePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) {
return notePathMatch[1];
}
return null;
}
function createNoteLink(notePath, noteTitle) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeUtils.getNoteTitle(noteId);
}
const noteLink = $("<a>", {
href: 'javascript:',
text: noteTitle
}).attr('action', 'note')
.attr('note-path', notePath);
return noteLink;
}
function goToLink(e) {
e.preventDefault();
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;
}
if (address.startsWith('http')) {
window.open(address, '_blank');
return;
}
notePath = getNotePathFromLink(address);
}
treeService.activateNode(notePath);
// this is quite ugly hack, but it seems like we can't close the tooltip otherwise
$("[role='tooltip']").remove();
if (glob.activeDialog) {
try {
glob.activeDialog.dialog('close');
}
catch (e) {}
}
}
function addLinkToEditor(linkTitle, linkHref) {
const editor = noteDetailText.getEditor();
editor.model.change( writer => {
const insertPosition = editor.model.document.selection.getFirstPosition();
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
});
}
function addTextToEditor(text) {
const editor = noteDetailText.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
}
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
// of opening the link in new window/tab
$(document).on('click', "a[action='note']", goToLink);
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '#note-detail-text a', goToLink);
export default {
getNodePathFromLabel,
getNotePathFromLink,
createNoteLink,
addLinkToEditor,
addTextToEditor
};

View File

@@ -0,0 +1,108 @@
import utils from './utils.js';
import infoService from "./info.js";
const $changesToPushCount = $("#changes-to-push-count");
const messageHandlers = [];
let ws;
let lastSyncId;
let lastPingTs;
function logError(message) {
console.log(utils.now(), message); // needs to be separate from .trace()
console.trace();
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
type: 'log-error',
error: message
}));
}
}
function subscribeToMessages(messageHandler) {
messageHandlers.push(messageHandler);
}
function handleMessage(event) {
const message = JSON.parse(event.data);
if (message.type === 'sync') {
lastPingTs = new Date().getTime();
if (message.data.length > 0) {
console.log(utils.now(), "Sync data: ", message.data);
lastSyncId = message.data[message.data.length - 1].id;
}
const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
for (const messageHandler of messageHandlers) {
messageHandler(syncData);
}
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
infoService.showError("Sync check failed!", 60000);
}
else if (message.type === 'consistency-checks-failed') {
infoService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
}
}
function connectWebSocket() {
const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
// use wss for secure messaging
const ws = new WebSocket(protocol + "://" + location.host);
ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
ws.onmessage = handleMessage;
ws.onclose = function(){
// Try to reconnect in 5 seconds
setTimeout(() => connectWebSocket(), 5000);
};
return ws;
}
setTimeout(() => {
ws = connectWebSocket();
lastSyncId = glob.maxSyncIdAtLoad;
lastPingTs = new Date().getTime();
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options
message: "Lost connection to server"
},{
// options
type: 'danger',
delay: 100000000 // keep it until we explicitly close it
});
}
}
else if (connectionBrokenNotification) {
await connectionBrokenNotification.close();
connectionBrokenNotification = null;
infoService.showMessage("Re-connected to server");
}
ws.send(JSON.stringify({
type: 'ping',
lastSyncId: lastSyncId
}));
}, 1000);
}, 0);
export default {
logError,
subscribeToMessages
};

View File

@@ -0,0 +1,283 @@
import treeService from './tree.js';
import treeUtils from './tree_utils.js';
import noteTypeService from './note_type.js';
import protectedSessionService from './protected_session.js';
import protectedSessionHolder from './protected_session_holder.js';
import utils from './utils.js';
import server from './server.js';
import messagingService from "./messaging.js";
import infoService from "./info.js";
import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js";
import noteDetailCode from './note_detail_code.js';
import noteDetailText from './note_detail_text.js';
import noteDetailFile from './note_detail_file.js';
import noteDetailSearch from './note_detail_search.js';
import noteDetailRender from './note_detail_render.js';
const $noteTitle = $("#note-title");
const $noteDetailComponents = $(".note-detail-component");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner");
const $childrenOverview = $("#children-overview");
let currentNote = null;
let noteChangeDisabled = false;
let isNoteChanged = false;
const components = {
'code': noteDetailCode,
'text': noteDetailText,
'file': noteDetailFile,
'search': noteDetailSearch,
'render': noteDetailRender
};
function getComponent(type) {
if (components[type]) {
return components[type];
}
else {
infoService.throwError("Unrecognized type: " + type);
}
}
function getCurrentNote() {
return currentNote;
}
function getCurrentNoteId() {
return currentNote ? currentNote.noteId : null;
}
function getCurrentNoteType() {
const currentNote = getCurrentNote();
return currentNote ? currentNote.type : null;
}
function noteChanged() {
if (noteChangeDisabled) {
return;
}
isNoteChanged = true;
}
async function reload() {
// no saving here
await loadNoteDetail(getCurrentNoteId());
}
async function switchToNote(noteId) {
if (getCurrentNoteId() !== noteId) {
await saveNoteIfChanged();
await loadNoteDetail(noteId);
}
}
async function saveNote() {
const note = getCurrentNote();
note.title = $noteTitle.val();
note.content = getComponent(note.type).getContent();
treeService.setNoteTitle(note.noteId, note.title);
await server.put('notes/' + note.noteId, note.dto);
isNoteChanged = false;
if (note.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
infoService.showMessage("Saved!");
}
async function saveNoteIfChanged() {
if (!isNoteChanged) {
return;
}
await saveNote();
}
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
function newNoteCreated() {
isNewNoteCreated = true;
}
async function handleProtectedSession() {
await protectedSessionService.ensureProtectedSession(currentNote.isProtected, false);
if (currentNote.isProtected) {
protectedSessionHolder.touchProtectedSession();
}
// this might be important if we focused on protected note when not in protected note and we got a dialog
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protectedSessionService.ensureDialogIsClosed();
}
async function loadNoteDetail(noteId) {
currentNote = await loadNote(noteId);
if (isNewNoteCreated) {
isNewNoteCreated = false;
$noteTitle.focus().select();
}
$noteIdDisplay.html(noteId);
await handleProtectedSession();
$noteDetailWrapper.show();
noteChangeDisabled = true;
try {
$noteTitle.val(currentNote.title);
noteTypeService.setNoteType(currentNote.type);
noteTypeService.setNoteMime(currentNote.mime);
$noteDetailComponents.hide();
await getComponent(currentNote.type).show();
}
finally {
noteChangeDisabled = false;
}
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
const labels = await loadLabelList();
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
await showChildrenOverview(hideChildrenOverview);
}
async function showChildrenOverview(hideChildrenOverview) {
if (hideChildrenOverview) {
$childrenOverview.hide();
return;
}
const note = getCurrentNote();
$childrenOverview.empty();
const notePath = treeService.getCurrentNotePath();
for (const childBranch of await note.getChildBranches()) {
const link = $('<a>', {
href: 'javascript:',
text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
}).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
}
$childrenOverview.show();
}
async function loadLabelList() {
const noteId = getCurrentNoteId();
const labels = await server.get('notes/' + noteId + '/labels');
$labelListInner.html('');
if (labels.length > 0) {
for (const label of labels) {
$labelListInner.append(utils.formatLabel(label) + " ");
}
$labelList.show();
}
else {
$labelList.hide();
}
return labels;
}
async function loadNote(noteId) {
const row = await server.get('notes/' + noteId);
return new NoteFull(treeCache, row);
}
function focus() {
const note = getCurrentNote();
getComponent(note.type).focus();
}
messagingService.subscribeToMessages(syncData => {
if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
infoService.showMessage('Reloading note because of background changes');
reload();
}
});
$(document).ready(() => {
$noteTitle.on('input', () => {
noteChanged();
const title = $noteTitle.val();
treeService.setNoteTitle(getCurrentNoteId(), title);
});
noteDetailText.focus();
});
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
// this sends the request asynchronously and doesn't wait for result
$(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise
setInterval(saveNoteIfChanged, 5000);
export default {
reload,
switchToNote,
setNoteBackgroundIfProtected,
loadNote,
getCurrentNote,
getCurrentNoteType,
getCurrentNoteId,
newNoteCreated,
focus,
loadLabelList,
saveNote,
saveNoteIfChanged,
noteChanged
};

View File

@@ -0,0 +1,95 @@
import libraryLoader from "./library_loader.js";
import bundleService from "./bundle.js";
import infoService from "./info.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
let codeEditor = null;
const $noteDetailCode = $('#note-detail-code');
const $executeScriptButton = $("#execute-script-button");
async function show() {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
if (!codeEditor) {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($noteDetailCode[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true,
tabindex: 2 // so that tab from title will lead to code editor focus
});
codeEditor.on('change', noteDetailService.noteChanged);
}
$noteDetailCode.show();
const currentNote = noteDetailService.getCurrentNote();
// this needs to happen after the element is shown, otherwise the editor won't be refreshed
codeEditor.setValue(currentNote.content);
const info = CodeMirror.findModeByMIME(currentNote.mime);
if (info) {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
function getContent() {
return codeEditor.getValue();
}
function focus() {
codeEditor.focus();
}
async function executeCurrentNote() {
if (noteDetailService.getCurrentNoteType() === 'code') {
// make sure note is saved so we load latest changes
await noteDetailService.saveNoteIfChanged();
const currentNote = noteDetailService.getCurrentNote();
if (currentNote.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
bundleService.executeBundle(bundle);
}
if (currentNote.mime.endsWith("env=backend")) {
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
}
infoService.showMessage("Note executed");
}
}
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
$executeScriptButton.click(executeCurrentNote);
export default {
show,
getContent,
focus
}

View File

@@ -0,0 +1,50 @@
import utils from "./utils.js";
import server from "./server.js";
import protectedSessionHolder from "./protected_session_holder.js";
import noteDetailService from "./note_detail.js";
const $noteDetailFile = $('#note-detail-file');
const $fileFileName = $("#file-filename");
const $fileFileType = $("#file-filetype");
const $fileFileSize = $("#file-filesize");
const $fileDownload = $("#file-download");
const $fileOpen = $("#file-open");
async function show() {
const currentNote = noteDetailService.getCurrentNote();
const labels = await server.get('notes/' + currentNote.noteId + '/labels');
const labelMap = utils.toObject(labels, l => [l.name, l.value]);
$noteDetailFile.show();
$fileFileName.text(labelMap.original_file_name);
$fileFileSize.text(labelMap.file_size + " bytes");
$fileFileType.text(currentNote.mime);
}
$fileDownload.click(() => utils.download(getFileUrl()));
$fileOpen.click(() => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl());
}
else {
window.location.href = getFileUrl();
}
});
function getFileUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return utils.getHost() + "/api/notes/" + noteDetailService.getCurrentNoteId()
+ "/download?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
}
export default {
show,
getContent: () => null,
focus: () => null
}

View File

@@ -0,0 +1,21 @@
import bundleService from "./bundle.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
const $noteDetailRender = $('#note-detail-render');
async function show() {
$noteDetailRender.show();
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
$noteDetailRender.html(bundle.html);
await bundleService.executeBundle(bundle);
}
export default {
show,
getContent: () => null,
focus: () => null
}

View File

@@ -0,0 +1,32 @@
import noteDetailService from "./note_detail.js";
const $searchString = $("#search-string");
const $noteDetailSearch = $('#note-detail-search');
function getContent() {
JSON.stringify({
searchString: $searchString.val()
});
}
function show() {
$noteDetailSearch.show();
try {
const json = JSON.parse(noteDetailService.getCurrentNote().content);
$searchString.val(json.searchString);
}
catch (e) {
console.log(e);
$searchString.val('');
}
$searchString.on('input', noteDetailService.noteChanged);
}
export default {
getContent,
show,
focus: () => null
}

View File

@@ -0,0 +1,47 @@
import libraryLoader from "./library_loader.js";
import noteDetailService from './note_detail.js';
const $noteDetailText = $('#note-detail-text');
let textEditor = null;
async function show() {
if (!textEditor) {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.model.document.on('change', noteDetailService.noteChanged);
}
textEditor.setData(noteDetailService.getCurrentNote().content);
$noteDetailText.show();
}
function getContent() {
let content = textEditor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
content = '';
}
return content;
}
function focus() {
$noteDetailText.focus();
}
function getEditor() {
return textEditor;
}
export default {
show,
getEditor,
getContent,
focus
}

View File

@@ -0,0 +1,146 @@
import treeService from './tree.js';
import noteDetailService from './note_detail.js';
import server from './server.js';
import infoService from "./info.js";
const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
const self = this;
this.type = ko.observable('text');
this.mime = ko.observable('');
this.codeMimeTypes = ko.observableArray([
{ mime: 'text/x-csrc', title: 'C' },
{ mime: 'text/x-c++src', title: 'C++' },
{ mime: 'text/x-csharp', title: 'C#' },
{ mime: 'text/x-clojure', title: 'Clojure' },
{ mime: 'text/css', title: 'CSS' },
{ mime: 'text/x-dockerfile', title: 'Dockerfile' },
{ mime: 'text/x-erlang', title: 'Erlang' },
{ mime: 'text/x-feature', title: 'Gherkin' },
{ mime: 'text/x-go', title: 'Go' },
{ mime: 'text/x-groovy', title: 'Groovy' },
{ mime: 'text/x-haskell', title: 'Haskell' },
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
{ mime: 'text/x-markdown', title: 'Markdown' },
{ mime: 'text/x-objectivec', title: 'Objective C' },
{ mime: 'text/x-pascal', title: 'Pascal' },
{ mime: 'text/x-perl', title: 'Perl' },
{ mime: 'text/x-php', title: 'PHP' },
{ mime: 'text/x-python', title: 'Python' },
{ mime: 'text/x-ruby', title: 'Ruby' },
{ mime: 'text/x-rustsrc', title: 'Rust' },
{ mime: 'text/x-scala', title: 'Scala' },
{ mime: 'text/x-sh', title: 'Shell' },
{ mime: 'text/x-sql', title: 'SQL' },
{ mime: 'text/x-swift', title: 'Swift' },
{ mime: 'text/xml', title: 'XML' },
{ mime: 'text/x-yaml', title: 'YAML' }
]);
this.typeString = function() {
const type = self.type();
const mime = self.mime();
if (type === 'text') {
return 'Text';
}
else if (type === 'code') {
if (!mime) {
return 'Code';
}
else {
const found = self.codeMimeTypes().find(x => x.mime === mime);
return found ? found.title : mime;
}
}
else if (type === 'render') {
return 'Render HTML note';
}
else if (type === 'file') {
return 'File';
}
else if (type === 'search') {
// ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
}
else {
infoService.throwError('Unrecognized type: ' + type);
}
};
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteDetailService.getCurrentNote();
await server.put('notes/' + note.noteId
+ '/type/' + encodeURIComponent(self.type())
+ '/mime/' + encodeURIComponent(self.mime()));
await noteDetailService.reload();
// for the note icon to be updated in the tree
await treeService.reload();
self.updateExecuteScriptButtonVisibility();
}
this.selectText = function() {
self.type('text');
self.mime('');
save();
};
this.selectRender = function() {
self.type('render');
self.mime('');
save();
};
this.selectCode = function() {
self.type('code');
self.mime('');
save();
};
this.selectCodeMime = function(el) {
self.type('code');
self.mime(el.mime);
save();
};
this.updateExecuteScriptButtonVisibility = function() {
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
}
}
ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
export default {
getNoteType: () => noteTypeModel.type(),
setNoteType: type => noteTypeModel.type(type),
getNoteMime: () => noteTypeModel.mime(),
setNoteMime: mime => {
noteTypeModel.mime(mime);
noteTypeModel.updateExecuteScriptButtonVisibility();
}
};

View File

@@ -0,0 +1,142 @@
import treeService from './tree.js';
import noteDetailService from './note_detail.js';
import utils from './utils.js';
import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js';
import infoService from "./info.js";
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $("#protected-session-password-form");
const $password = $("#protected-session-password");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
let protectedSessionDeferred = null;
function ensureProtectedSession(requireProtectedSession, modal) {
const dfd = $.Deferred();
if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
if (treeService.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
$dialog.dialog({
modal: modal,
width: 400,
open: () => {
if (!modal) {
// dialog steals focus for itself, which is not what we want for non-modal (viewing)
treeService.getCurrentNode().setFocus();
}
}
});
}
else {
dfd.resolve();
}
return dfd.promise();
}
async function setupProtectedSession() {
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
if (!response.success) {
infoService.showError("Wrong password.");
return;
}
protectedSessionHolder.setProtectedSessionId(response.protectedSessionId);
$dialog.dialog("close");
noteDetailService.reload();
treeService.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed($dialog, $password);
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
}
}
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
$dialog.dialog('close');
}
catch (e) {}
$password.val('');
}
async function enterProtectedSession(password) {
return await server.post('login/protected', {
password: password
});
}
async function protectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
note.isProtected = true;
await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected);
noteDetailService.setNoteBackgroundIfProtected(note);
}
async function unprotectNoteAndSendToServer() {
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
note.isProtected = false;
await noteDetailService.saveNote(note);
treeService.setProtected(note.noteId, note.isProtected);
noteDetailService.setNoteBackgroundIfProtected(note);
}
async function protectBranch(noteId, protect) {
await ensureProtectedSession(true, true);
await server.put('notes/' + noteId + "/protect/" + (protect ? 1 : 0));
infoService.showMessage("Request to un/protect sub tree has finished successfully");
treeService.reload();
noteDetailService.reload();
}
$passwordForm.submit(() => {
setupProtectedSession();
return false;
});
$protectButton.click(protectNoteAndSendToServer);
$unprotectButton.click(unprotectNoteAndSendToServer);
export default {
ensureProtectedSession,
protectNoteAndSendToServer,
unprotectNoteAndSendToServer,
protectBranch,
ensureDialogIsClosed
};

View File

@@ -0,0 +1,55 @@
import utils from "./utils.js";
import server from "./server.js";
let lastProtectedSessionOperationDate = null;
let protectedSessionTimeout = null;
let protectedSessionId = null;
$(document).ready(() => {
server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
});
setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
resetProtectedSession();
}
}, 5000);
function setProtectedSessionTimeout(encSessTimeout) {
protectedSessionTimeout = encSessTimeout;
}
function getProtectedSessionId() {
return protectedSessionId;
}
function setProtectedSessionId(id) {
protectedSessionId = id;
}
function resetProtectedSession() {
protectedSessionId = null;
// most secure solution - guarantees nothing remained in memory
// since this expires because user doesn't use the app, it shouldn't be disruptive
utils.reloadApp();
}
function isProtectedSessionAvailable() {
return protectedSessionId !== null;
}
function touchProtectedSession() {
if (isProtectedSessionAvailable()) {
lastProtectedSessionOperationDate = new Date();
}
}
export default {
getProtectedSessionId,
setProtectedSessionId,
resetProtectedSession,
isProtectedSessionAvailable,
setProtectedSessionTimeout,
touchProtectedSession
};

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