Compare commits

...

137 Commits

Author SHA1 Message Date
azivner
3d185a5178 release 0.18.0 2018-07-30 08:18:25 +02:00
azivner
2ff7a890bc script which shows list of edited files for a date, fixes #125 2018-07-29 20:51:28 +02:00
azivner
2eb1a9705f it's now possible to mark relation as inheritable (previously this was hardcoded for specific relation names) 2018-07-29 20:33:42 +02:00
azivner
ed1381103a #125, implementation of inheritable relations 2018-07-29 18:39:10 +02:00
azivner
170d317589 #125, basic infrastructure for scripts attached to notes via relations 2018-07-29 16:06:13 +02:00
azivner
ededc063df fix relation autocomplete 2018-07-29 12:34:40 +02:00
azivner
986eace1be schema update with relations 2018-07-29 11:47:46 +02:00
azivner
29086d8dfe fixed bug with select not firing on recent notes 2018-07-28 18:17:35 +02:00
azivner
9b3f3fde05 #126, relation list in note detail and fixes in saving 2018-07-28 17:59:55 +02:00
azivner
6a50afd952 #126, "show recent notes" now work in relations dialog 2018-07-28 17:02:48 +02:00
azivner
697eee2706 #126, autocomplete + can save relations 2018-07-27 11:28:24 +02:00
azivner
8a95afd756 #126, added skeleton of note relations, copied from similar concept of labels 2018-07-27 10:52:48 +02:00
azivner
4d6eda8fe6 #129, added input icon to trigger recent notes for easy discovery and mouse control 2018-07-27 09:22:25 +02:00
azivner
e4f459fa2b #129, removed recent notes dialog as its not necessary anymore 2018-07-26 17:35:32 +02:00
azivner
f578e001b0 #129, add link autocomplete now displays recent notes as well 2018-07-26 16:24:08 +02:00
azivner
2a08aef885 #129, recent notes are now visible in the jump to dialog 2018-07-26 16:05:09 +02:00
azivner
7564bf388c removed dangerous and unnecessary option to completely remove soft-deleted items 2018-07-26 09:21:52 +02:00
azivner
7e4d70259f soft-deleting note will delete its content and all the revisions content, fixes #132 2018-07-26 09:08:51 +02:00
azivner
5b98c1c0f3 fix context menu JS error on root note 2018-07-26 08:58:20 +02:00
azivner
02dc7b199b #98, better error reporting for sync setup 2018-07-25 22:54:37 +02:00
azivner
d39cdbfada better instructions for desktop => server instance sync setup, plus some sync fixes 2018-07-25 10:57:36 +02:00
azivner
50bb4a47ee fix sync bug 2018-07-25 10:12:34 +02:00
azivner
a4627f2ddb #98 some sync setup refactorings 2018-07-25 09:46:57 +02:00
azivner
c8253caae9 #98 proxy support for sync setup 2018-07-25 08:30:41 +02:00
azivner
0ece9bd1be sync isExpanded when it's a new branch 2018-07-24 22:03:36 +02:00
azivner
b6935abcc9 #98, sync button now shows total number of outstanding syncs instead of just pushes 2018-07-24 21:43:15 +02:00
azivner
37ab7b4641 #98, sync to server now works as well + a lot of related changes 2018-07-24 20:35:03 +02:00
azivner
013714cb5c #98, new option "initialized" which indicates if setup has been finished 2018-07-24 08:12:36 +02:00
azivner
1fe7c62f5a #98, sync setup now doesn't copy the whole DB file, but sets up minimal database and starts off sync 2018-07-23 21:15:32 +02:00
azivner
a06618d851 #98, test sync impl 2018-07-23 10:29:17 +02:00
azivner
e7460ca3a9 #98, sync is now configured in the options 2018-07-22 22:21:16 +02:00
azivner
073300bbcd #98, working sync setup from server to desktop instance + refactoring of DB initialization 2018-07-22 19:56:20 +02:00
azivner
a201661ce5 #98, fixes in the wizard 2018-07-22 14:49:59 +02:00
azivner
6235a3c886 beginning of #98, new multistep wizard, db creation after user enters username and password 2018-07-21 08:55:24 +02:00
azivner
3972c27e7a release 0.17.0 2018-07-09 21:22:12 +02:00
azivner
14cffbbe62 docker commands with sudo 2018-07-09 21:21:28 +02:00
azivner
599c3c04af correct sourceMappingURL which gets rid of error message, fixes #114 2018-07-08 23:23:49 +02:00
azivner
f1412b631d hide log polluting error message 2018-07-08 23:13:56 +02:00
azivner
41908050bb ctrl+u now doesn't trigger show source since it's occupied by underline 2018-07-08 21:45:05 +02:00
azivner
f07033423c ckeditor upgraded to 10.1.0 plus new plugins (table, strikethrough, underline) 2018-07-08 21:21:52 +02:00
azivner
daf96fcbf2 using zeit/pkg to package easy to use linux server edition 2018-07-06 00:05:06 +02:00
azivner
2bca94529e minor upgrades 2018-07-05 23:17:53 +02:00
azivner
b2c9a0da21 fix note sources, fixes #123 2018-07-04 20:37:23 +02:00
azivner
7a9542b4fc release 0.16.0 2018-06-16 13:34:39 -04:00
azivner
3a95c9e1bc all dialogs are now non-modal because of high cpu usage 2018-06-16 13:31:56 -04:00
azivner
3d2ef6be01 remove optionId, closes #117 2018-06-13 19:10:28 -04:00
azivner
d67246699a Introduced separate sync version (previously DB version was used to check sync compatibility), closes #120 2018-06-10 15:55:29 -04:00
azivner
14c704d6db db upgrades are now handled transparently in the background without bothering the user, closes #119 2018-06-10 15:49:22 -04:00
azivner
4c8eeb2e6f added docker build, closes #106 2018-06-10 15:06:52 -04:00
azivner
c1b245c8b1 fix unnecessary change events, closes #118 2018-06-10 11:51:13 -04:00
azivner
74202d67bb got rid of "Trilium Notes" branding - not necessary and takes valuable space 2018-06-10 10:57:45 -04:00
azivner
26066f39b1 chaged "focused mode" - now title is displayed as well and together with content takes whole window 2018-06-10 10:53:39 -04:00
azivner
b255cf190c fixes for zoom factor setting 2018-06-09 10:34:51 -04:00
azivner
bc77b143b0 darker outlines so inverted dark themes are more visible 2018-06-09 09:48:18 -04:00
azivner
9f0ff6ae7a note actions dropdown sizing 2018-06-09 09:44:40 -04:00
azivner
736704c7d6 fix show paths 2018-06-09 09:32:13 -04:00
azivner
654c116c58 use backgrounds for icon buttons so that dark and black themes look better 2018-06-09 09:28:50 -04:00
azivner
89a5cab98f added too options new tab appearance with possibility to change theme (white, black, dark) and zoom factor 2018-06-08 23:18:53 -04:00
azivner
c39d0be8cd refactoring of icon button styles 2018-06-08 22:17:00 -04:00
azivner
e75b4cd848 execute on script note is icon, closes #116 2018-06-08 21:59:40 -04:00
azivner
378e8f35e5 release 0.15.0 2018-06-07 23:09:21 -04:00
azivner
bdb5e2f13f no results also for "add link" 2018-06-07 23:08:41 -04:00
azivner
8211bed449 renamed icons according to their size, fixes #113 2018-06-07 23:02:21 -04:00
azivner
b243632483 usability improvements to autocomplete ("no results" etc.), needs refactoring 2018-06-07 20:18:46 -04:00
azivner
e4d2513451 close search button 2018-06-07 19:50:16 -04:00
azivner
385144451b renamed hideInAutocomplete label to archived 2018-06-07 19:26:28 -04:00
azivner
c8c533844e icons for render & toggle edit since text titles caused unwanted horizontal scrolling in smaller window sizes 2018-06-06 23:37:57 -04:00
azivner
0e69f0c079 fix recent notes issues 2018-06-06 22:38:36 -04:00
azivner
aee60c444f added "show results in full text" 2018-06-05 23:28:10 -04:00
azivner
e7a504c66b fixes and optimizations for search 2018-06-05 22:47:47 -04:00
azivner
45d9c7164c search refactorings 2018-06-05 19:12:52 -04:00
azivner
bd913a63a8 search note fixes 2018-06-04 23:21:45 -04:00
azivner
5a1938c078 better sizing of search pane 2018-06-04 20:22:41 -04:00
azivner
015cd68756 renaming/refactoring of search services 2018-06-04 19:48:02 -04:00
azivner
76c0e5b2b8 new UI for search, closes #108 (still needs cleanup) 2018-06-03 20:42:25 -04:00
azivner
0f8f707acd persisting zoom setting in electron, fixes #112 2018-06-02 13:02:20 -04:00
azivner
083cccea28 better protected/unprotected note indicator, fixes #110 2018-06-02 11:47:16 -04:00
azivner
31b76b23ce release 0.14.1 2018-06-02 09:39:37 -04:00
azivner
af529f82e5 fixed false sync error reporting 2018-06-02 09:39:04 -04:00
azivner
fc6669d254 initialization and schema fixes, closes #111 2018-06-01 22:26:37 -04:00
azivner
c07785be67 release 0.14.0 2018-05-31 23:23:44 -04:00
azivner
80d2457b23 moved parent list next to note title 2018-05-31 23:21:47 -04:00
azivner
5dde2752d2 add switch to manually enter/leave protected session, fixes #107 2018-05-31 20:00:39 -04:00
azivner
8bf4633cd0 fixes 2018-05-30 23:18:56 -04:00
azivner
bd66b8a1c8 fix issue with limitation of number of SQLite parameters (999) which caused problems when loading tree which was too expanded 2018-05-30 20:28:10 -04:00
azivner
be51e533fc OPML import support (issue #78) 2018-05-29 20:32:13 -04:00
azivner
f47ae12019 OPML export support (issue #78), import missing for now 2018-05-27 12:26:34 -04:00
azivner
cab54a458f unifying surrogate keys for event log and options, fixes #103 2018-05-26 23:25:09 -04:00
azivner
a30734f1bc Add history backwards/forwards buttons, fixes #94 2018-05-26 22:54:06 -04:00
azivner
7ad9f7b129 fixed layouting issues 2018-05-26 19:58:08 -04:00
azivner
40a32e6826 render notes can be edited and can contain HTML markup 2018-05-26 19:27:47 -04:00
azivner
ab0486aaf1 expose root node, fixes #101 2018-05-26 16:16:34 -04:00
azivner
874593a167 fix code editor growing 2018-05-26 15:28:36 -04:00
azivner
03bf33630e unify audit fields, fixes #102 2018-05-26 12:38:25 -04:00
azivner
933cce1b94 fix hideInAutocomplete bug 2018-05-26 10:50:13 -04:00
azivner
4a6ff573f8 fixed autocomplete issues with capitalization 2018-05-26 10:24:33 -04:00
azivner
1a737f7d19 expose add link on UI, fixes #95 2018-05-26 10:04:40 -04:00
azivner
cb69914f09 release 0.13.0-beta 2018-05-22 23:51:43 -04:00
azivner
a372cbb2df fix #105 2018-05-22 23:51:13 -04:00
azivner
0ce5caefe8 refactoring 2018-05-22 22:22:15 -04:00
azivner
94dabb81f6 fix sync of unsyncable options 2018-05-22 19:29:18 -04:00
azivner
cd45bcfd03 converted option operations to repository 2018-05-22 00:22:43 -04:00
azivner
49a53f7a45 added hash columns for faster sync check calculation 2018-05-22 00:15:54 -04:00
azivner
9fa6c0918c add index for note's type + some fixes 2018-05-21 20:12:46 -04:00
azivner
e8d089e37e ckeditor 10.0.0 2018-05-21 19:35:49 -04:00
azivner
a931ce25fa attempt to fix the hoek security warning with package upgrade 2018-05-21 16:08:34 -04:00
azivner
b507abb4f7 electron upgrade to 2.0.0 2018-05-08 16:39:01 -04:00
azivner
66e7c6de62 fix ordering 2018-04-21 12:23:35 -04:00
azivner
4ce5ea9886 autocomplete supports encrypted notes now as well 2018-04-20 00:12:01 -04:00
azivner
8c54b62f07 fix protect branch 2018-04-19 22:18:19 -04:00
azivner
85eb50ed0f autocomplete with prefixes 2018-04-19 20:59:44 -04:00
azivner
5ffd621e9d autocomplete respects hideInAutocomplete label 2018-04-19 00:13:55 -04:00
azivner
df93cb09da fix hide-toggle 2018-04-18 23:13:37 -04:00
azivner
bbf04209f0 autocomplete cache gets updated with note update 2018-04-18 23:11:30 -04:00
azivner
834bfa39c7 limit number of results to 200, other tweaks 2018-04-18 20:56:23 -04:00
azivner
52b445f70b Merge branch 'stable' 2018-04-18 20:22:16 -04:00
azivner
7b9b4fbb0c backend autocomplete WIP 2018-04-18 00:26:42 -04:00
azivner
5af0ba1fcb layout fixes 2018-04-17 20:04:27 -04:00
azivner
85a9748291 fix for clones & optimizations 2018-04-16 23:34:56 -04:00
azivner
b4005a7ffe optimizations to the lazy loading - expanding tree now takes only one request 2018-04-16 23:13:33 -04:00
azivner
82de1c88d4 basic lazy loading of tree now works, still WIP 2018-04-16 20:40:18 -04:00
azivner
1687ed7e0b load only expanded tree with the rest being lazy loaded, WIP 2018-04-16 16:26:47 -04:00
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
177 changed files with 7937 additions and 3374 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
npm-debug.log
dist
.idea

View File

@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="document.db">
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.7">
<root id="1"/>
<database-model serializer="dbm" rdbms="SQLITE" format-version="4.9">
<root id="1">
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
<Visible>1</Visible>
@@ -19,568 +21,718 @@
<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">
<table id="16" parent="2" name="relations"/>
<table id="17" parent="2" name="source_ids"/>
<table id="18" parent="2" name="sqlite_master">
<System>1</System>
</table>
<table id="18" parent="2" name="sqlite_sequence">
<table id="19" parent="2" name="sqlite_sequence">
<System>1</System>
</table>
<table id="19" parent="2" name="sync"/>
<column id="20" parent="6" name="apiTokenId">
<table id="20" parent="2" name="sync"/>
<column id="21" parent="6" name="apiTokenId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="21" parent="6" name="token">
<column id="22" parent="6" name="token">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="22" parent="6" name="dateCreated">
<column id="23" parent="6" name="dateCreated">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="23" parent="6" name="isDeleted">
<column id="24" 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">
<column id="25" parent="6" name="hash">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="26" 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">
<key id="27" parent="6">
<ColNames>apiTokenId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
</key>
<column id="26" parent="7" name="branchId">
<column id="28" parent="7" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="27" parent="7" name="noteId">
<column id="29" parent="7" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="28" parent="7" name="parentNoteId">
<column id="30" parent="7" name="parentNoteId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="29" parent="7" name="notePosition">
<column id="31" parent="7" name="notePosition">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="30" parent="7" name="prefix">
<column id="32" parent="7" name="prefix">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="31" parent="7" name="isExpanded">
<column id="33" parent="7" name="isExpanded">
<Position>6</Position>
<DataType>BOOLEAN|0s</DataType>
</column>
<column id="32" parent="7" name="isDeleted">
<column id="34" 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">
<column id="35" 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">
<column id="36" parent="7" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="37" parent="7" name="dateCreated">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</DefaultExpression>
</column>
<index id="38" 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">
<index id="39" parent="7" name="IDX_branches_noteId_parentNoteId">
<ColNames>noteId
parentNoteId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<index id="36" parent="7" name="IDX_branches_noteId">
<index id="40" parent="7" name="IDX_branches_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="37" parent="7">
<index id="41" parent="7" name="IDX_branches_parentNoteId">
<ColNames>parentNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="42" parent="7">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="38" parent="8" name="id">
<column id="43" parent="8" name="eventId">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="39" parent="8" name="noteId">
<column id="44" parent="8" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="40" parent="8" name="comment">
<column id="45" parent="8" name="comment">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="41" parent="8" name="dateAdded">
<column id="46" parent="8" name="dateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="42" parent="8">
<ColNames>id</ColNames>
<index id="47" parent="8" name="sqlite_autoindex_event_log_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>eventId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="48" parent="8">
<ColNames>eventId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_event_log_1</UnderlyingIndexName>
</key>
<foreign-key id="43" parent="8">
<ColNames>noteId</ColNames>
<RefTableName>notes</RefTableName>
<RefColNames>noteId</RefColNames>
</foreign-key>
<column id="44" parent="9" name="imageId">
<column id="49" parent="9" name="imageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="45" parent="9" name="format">
<column id="50" parent="9" name="format">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="46" parent="9" name="checksum">
<column id="51" parent="9" name="checksum">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="47" parent="9" name="name">
<column id="52" parent="9" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="48" parent="9" name="data">
<column id="53" parent="9" name="data">
<Position>5</Position>
<DataType>BLOB|0s</DataType>
</column>
<column id="49" parent="9" name="isDeleted">
<column id="54" parent="9" name="isDeleted">
<Position>6</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="50" parent="9" name="dateModified">
<column id="55" parent="9" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="9" name="dateCreated">
<column id="56" parent="9" name="dateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="52" parent="9" name="sqlite_autoindex_images_1">
<column id="57" parent="9" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="58" parent="9" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="53" parent="9">
<key id="59" parent="9">
<ColNames>imageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key>
<column id="54" parent="10" name="labelId">
<column id="60" parent="10" name="labelId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="55" parent="10" name="noteId">
<column id="61" parent="10" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="56" parent="10" name="name">
<column id="62" parent="10" name="name">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="57" parent="10" name="value">
<column id="63" parent="10" name="value">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="58" parent="10" name="position">
<column id="64" parent="10" name="position">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="59" parent="10" name="dateCreated">
<column id="65" parent="10" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="60" parent="10" name="dateModified">
<column id="66" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="61" parent="10" name="isDeleted">
<column id="67" parent="10" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="62" parent="10" name="sqlite_autoindex_labels_1">
<column id="68" parent="10" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="69" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="63" parent="10" name="IDX_labels_noteId">
<index id="70" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="64" parent="10" name="IDX_labels_name_value">
<index id="71" parent="10" name="IDX_labels_name_value">
<ColNames>name
value</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<key id="65" parent="10">
<key id="72" parent="10">
<ColNames>labelId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key>
<column id="66" parent="11" name="noteImageId">
<column id="73" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="67" parent="11" name="noteId">
<column id="74" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="68" parent="11" name="imageId">
<column id="75" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="69" parent="11" name="isDeleted">
<column id="76" parent="11" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="70" parent="11" name="dateModified">
<column id="77" parent="11" name="dateModified">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="71" parent="11" name="dateCreated">
<column id="78" parent="11" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="72" parent="11" name="sqlite_autoindex_note_images_1">
<column id="79" parent="11" name="hash">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="80" parent="11" name="sqlite_autoindex_note_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteImageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="73" parent="11" name="IDX_note_images_noteId_imageId">
<index id="81" parent="11" name="IDX_note_images_noteId_imageId">
<ColNames>noteId
imageId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
</index>
<index id="74" parent="11" name="IDX_note_images_noteId">
<index id="82" parent="11" name="IDX_note_images_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="75" parent="11" name="IDX_note_images_imageId">
<index id="83" parent="11" name="IDX_note_images_imageId">
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="76" parent="11">
<key id="84" parent="11">
<ColNames>noteImageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key>
<column id="77" parent="12" name="noteRevisionId">
<column id="85" parent="12" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="78" parent="12" name="noteId">
<column id="86" parent="12" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="79" parent="12" name="title">
<column id="87" parent="12" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="80" parent="12" name="content">
<column id="88" parent="12" name="content">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="81" parent="12" name="isProtected">
<column id="89" parent="12" name="isProtected">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="82" parent="12" name="dateModifiedFrom">
<column id="90" parent="12" name="dateModifiedFrom">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="83" parent="12" name="dateModifiedTo">
<column id="91" parent="12" name="dateModifiedTo">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="84" parent="12" name="sqlite_autoindex_note_revisions_1">
<column id="92" parent="12" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="93" parent="12" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;&apos;</DefaultExpression>
</column>
<column id="94" parent="12" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="95" parent="12" name="sqlite_autoindex_note_revisions_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteRevisionId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="85" parent="12" name="IDX_note_revisions_noteId">
<index id="96" parent="12" name="IDX_note_revisions_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="86" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<index id="97" parent="12" name="IDX_note_revisions_dateModifiedFrom">
<ColNames>dateModifiedFrom</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="87" parent="12" name="IDX_note_revisions_dateModifiedTo">
<index id="98" parent="12" name="IDX_note_revisions_dateModifiedTo">
<ColNames>dateModifiedTo</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="88" parent="12">
<key id="99" parent="12">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="89" parent="13" name="noteId">
<column id="100" parent="13" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" parent="13" name="title">
<column id="101" parent="13" name="title">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;unnamed&quot;</DefaultExpression>
</column>
<column id="91" parent="13" name="content">
<column id="102" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="92" parent="13" name="isProtected">
<column id="103" parent="13" name="isProtected">
<Position>4</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="93" parent="13" name="isDeleted">
<column id="104" parent="13" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="94" parent="13" name="dateCreated">
<column id="105" parent="13" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="95" parent="13" name="dateModified">
<column id="106" parent="13" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="96" parent="13" name="type">
<column id="107" parent="13" name="type">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text&apos;</DefaultExpression>
</column>
<column id="97" parent="13" name="mime">
<column id="108" parent="13" name="mime">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;text/html&apos;</DefaultExpression>
</column>
<index id="98" parent="13" name="sqlite_autoindex_notes_1">
<column id="109" parent="13" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="110" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="99" parent="13" name="IDX_notes_isDeleted">
<ColNames>isDeleted</ColNames>
<index id="111" parent="13" name="IDX_notes_type">
<ColNames>type</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="100" parent="13">
<key id="112" parent="13">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="101" parent="14" name="name">
<column id="113" parent="14" name="name">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="102" parent="14" name="value">
<column id="114" parent="14" name="value">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="103" parent="14" name="dateModified">
<column id="115" parent="14" name="dateModified">
<Position>3</Position>
<DataType>INT|0s</DataType>
</column>
<column id="104" parent="14" name="isSynced">
<column id="116" parent="14" name="isSynced">
<Position>4</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<index id="105" parent="14" name="sqlite_autoindex_options_1">
<column id="117" parent="14" name="hash">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="118" parent="14" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&apos;1970-01-01T00:00:00.000Z&apos;</DefaultExpression>
</column>
<index id="119" parent="14" name="sqlite_autoindex_options_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>name</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="106" parent="14">
<key id="120" parent="14">
<ColNames>name</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
</key>
<column id="107" parent="15" name="branchId">
<column id="121" parent="15" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="108" parent="15" name="notePath">
<column id="122" parent="15" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="109" parent="15" name="dateAccessed">
<column id="123" parent="15" name="hash">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="110" parent="15" name="isDeleted">
<column id="124" parent="15" name="dateCreated">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="125" parent="15" name="isDeleted">
<Position>5</Position>
<DataType>INT|0s</DataType>
</column>
<index id="111" parent="15" name="sqlite_autoindex_recent_notes_1">
<index id="126" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="112" parent="15">
<key id="127" parent="15">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="113" parent="16" name="sourceId">
<column id="128" parent="16" name="relationId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="114" parent="16" name="dateCreated">
<column id="129" parent="16" name="sourceNoteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="115" parent="16" name="sqlite_autoindex_source_ids_1">
<column id="130" parent="16" name="name">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="131" parent="16" name="targetNoteId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="132" parent="16" name="position">
<Position>5</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>0</DefaultExpression>
</column>
<column id="133" parent="16" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="134" parent="16" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="135" parent="16" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="136" parent="16" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="137" parent="16" name="sqlite_autoindex_relations_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>relationId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="138" parent="16" name="IDX_relation_sourceNoteId">
<ColNames>sourceNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="139" parent="16" name="IDX_relation_targetNoteId">
<ColNames>targetNoteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="140" parent="16">
<ColNames>relationId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_relations_1</UnderlyingIndexName>
</key>
<column id="141" parent="17" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="142" parent="17" name="dateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="143" parent="17" name="sqlite_autoindex_source_ids_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>sourceId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="116" parent="16">
<key id="144" parent="17">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="117" parent="17" name="type">
<column id="145" parent="18" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="118" parent="17" name="name">
<column id="146" parent="18" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="119" parent="17" name="tbl_name">
<column id="147" parent="18" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="120" parent="17" name="rootpage">
<column id="148" parent="18" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="121" parent="17" name="sql">
<column id="149" parent="18" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="122" parent="18" name="name">
<column id="150" parent="19" name="name">
<Position>1</Position>
</column>
<column id="123" parent="18" name="seq">
<column id="151" parent="19" name="seq">
<Position>2</Position>
</column>
<column id="124" parent="19" name="id">
<column id="152" parent="20" name="id">
<Position>1</Position>
<DataType>INTEGER|0s</DataType>
<NotNull>1</NotNull>
<SequenceIdentity>1</SequenceIdentity>
</column>
<column id="125" parent="19" name="entityName">
<column id="153" parent="20" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="126" parent="19" name="entityId">
<column id="154" parent="20" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="19" name="sourceId">
<column id="155" parent="20" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="128" parent="19" name="syncDate">
<column id="156" parent="20" name="syncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="129" parent="19" name="IDX_sync_entityName_entityId">
<index id="157" parent="20" name="IDX_sync_entityName_entityId">
<ColNames>entityName
entityId</ColNames>
<ColumnCollations>
</ColumnCollations>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="130" parent="19" name="IDX_sync_syncDate">
<index id="158" parent="20" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="131" parent="19">
<key id="159" parent="20">
<ColNames>id</ColNames>
<Primary>1</Primary>
</key>

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM node:8.11.2
RUN apt-get update && apt-get install -y nasm
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install --production
# If you are building your code for production
# RUN npm install --only=production
# Bundle app source
COPY . .
EXPOSE 8080
CMD [ "node", "src/www" ]

8
bin/build-docker.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
sudo docker build -t zadam/trilium:latest -t zadam/trilium:$1 .

16
bin/build-pkg.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
mkdir $PKG_DIR
pkg . --targets node8-linux-x64 --output ${PKG_DIR}/trilium
chmod +x ${PKG_DIR}/trilium
cp node_modules/sqlite3/lib/binding/node-v57-linux-x64/node_sqlite3.node ${PKG_DIR}/
cp node_modules/scrypt/build/Release/scrypt.node ${PKG_DIR}/
cd dist
7z a trilium-linux-x64-server.7z trilium-linux-x64-server

9
bin/push-docker-image.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
if [[ $# -eq 0 ]] ; then
echo "Missing argument of new version"
exit 1
fi
sudo docker push zadam/trilium:latest
sudo docker push zadam/trilium:$1

View File

@@ -47,6 +47,7 @@ bin/package.sh
LINUX_X64_BUILD=trilium-linux-x64-$VERSION.7z
LINUX_IA32_BUILD=trilium-linux-ia32-$VERSION.7z
WINDOWS_X64_BUILD=trilium-windows-x64-$VERSION.7z
SERVER_BUILD=trilium-linux-x64-server.elf
echo "Creating release in GitHub"
@@ -75,4 +76,21 @@ github-release upload \
--name "$WINDOWS_X64_BUILD" \
--file "dist/$WINDOWS_X64_BUILD"
echo "Packaging server version"
npm run build-pkg
github-release upload \
--tag $TAG \
--name "$SERVER_BUILD" \
--file "dist/$SERVER_BUILD"
echo "Building docker image"
bin/build-docker.sh $VERSION
echo "Pushing docker image to dockerhub"
bin/push-docker-image.sh $VERSION
echo "Release finished!"

View File

@@ -9,9 +9,3 @@ https=false
# path to certificate (run "bash generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
certPath=
keyPath=
[Sync]
syncServerHost=
syncServerTimeout=10000
syncProxy=
syncServerCertificate=

View File

@@ -1,3 +1,4 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
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');

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

@@ -0,0 +1,5 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');

View File

@@ -0,0 +1 @@
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);

View File

@@ -0,0 +1,2 @@
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
DROP INDEX IDX_notes_isDeleted;

View File

@@ -0,0 +1,2 @@
create index IDX_notes_type
on notes (type);

View File

@@ -0,0 +1,9 @@
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;

View File

@@ -0,0 +1,30 @@
ALTER TABLE branches ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
CREATE TABLE `event_log_mig` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
INSERT INTO event_log_mig (id, noteId, comment, dateCreated)
SELECT id, noteId, comment, dateAdded FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_log_mig RENAME TO event_log;
ALTER TABLE options ADD dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z';
CREATE TABLE `recent_notes_mig` (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
`dateCreated` TEXT NOT NULL,
isDeleted INT
);
INSERT INTO recent_notes_mig (branchId, notePath, hash, dateCreated, isDeleted)
SELECT branchId, notePath, hash, dateAccessed, isDeleted FROM recent_notes;
DROP TABLE recent_notes;
ALTER TABLE recent_notes_mig RENAME TO recent_notes;

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'text/html' WHERE type = 'render';

View File

@@ -0,0 +1,29 @@
CREATE TABLE `event_log_mig` (
`eventId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
INSERT INTO event_log_mig (eventId, noteId, comment, dateCreated)
SELECT id, noteId, comment, dateCreated FROM event_log;
DROP TABLE event_log;
ALTER TABLE event_log_mig RENAME TO event_log;
create table options_mig
(
optionId TEXT NOT NULL PRIMARY KEY,
name TEXT not null,
value TEXT,
dateModified INT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
);
INSERT INTO options_mig (optionId, name, value, dateModified, isSynced, hash, dateCreated)
SELECT name || "_key", name, value, dateModified, isSynced, hash, dateCreated FROM options;
DROP TABLE options;
ALTER TABLE options_mig RENAME TO options;

View File

@@ -0,0 +1,2 @@
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
VALUES ('zoomFactor_key', 'zoomFactor', '1.0', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1 @@
UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete'

View File

@@ -0,0 +1,2 @@
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
VALUES ('theme_key', 'theme', 'white', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,15 @@
create table options_mig
(
name TEXT not null PRIMARY KEY,
value TEXT,
dateModified INT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
);
INSERT INTO options_mig (name, value, dateModified, isSynced, hash, dateCreated)
SELECT name, value, dateModified, isSynced, hash, dateCreated FROM options;
DROP TABLE options;
ALTER TABLE options_mig RENAME TO options;

View File

@@ -0,0 +1,8 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncServerHost', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncServerTimeout', '5000', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('syncProxy', '', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,2 @@
DELETE FROM sync WHERE entityName = 'note_tree';
DELETE FROM sync WHERE entityName = 'attributes';

View File

@@ -0,0 +1,2 @@
INSERT INTO options (name, value, dateCreated, dateModified, isSynced)
VALUES ('initialized', 'true', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);

View File

@@ -0,0 +1,4 @@
const syncTableService = require('../../src/services/sync_table');
// options has not been filled so far which caused problems with clean-slate sync.
module.exports = async () => await syncTableService.fillAllSyncRows();

View File

@@ -0,0 +1,2 @@
UPDATE notes SET content = '' WHERE isDeleted = 1;
UPDATE note_revisions SET content = '' WHERE (SELECT isDeleted FROM notes WHERE noteId = note_revisions.noteId) = 1;

View File

@@ -0,0 +1,15 @@
CREATE TABLE relations
(
relationId TEXT not null primary key,
sourceNoteId TEXT not null,
name TEXT not null,
targetNoteId TEXT not null,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX IDX_relation_sourceNoteId
on relations (sourceNoteId);
CREATE INDEX IDX_relation_targetNoteId
on relations (targetNoteId);

View File

@@ -0,0 +1 @@
ALTER TABLE relations ADD isInheritable int DEFAULT 0 NULL;

View File

@@ -1,8 +1,3 @@
CREATE TABLE IF NOT EXISTS "options" (
`name` TEXT NOT NULL PRIMARY KEY,
`value` TEXT,
`dateModified` INT,
isSynced INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL,
@@ -21,28 +16,6 @@ CREATE TABLE IF NOT EXISTS "source_ids" (
`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 INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
);
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_revisions" (
`noteRevisionId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT NOT NULL,
@@ -51,7 +24,7 @@ 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, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId`
);
@@ -71,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
, hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE note_images
(
noteImageId TEXT PRIMARY KEY NOT NULL,
@@ -80,7 +53,7 @@ CREATE TABLE note_images
isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL
);
, hash TEXT DEFAULT "" NOT NULL);
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);
@@ -90,7 +63,7 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
token TEXT NOT NULL,
dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0
);
, hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL,
@@ -99,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
`prefix` TEXT,
`isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0,
`dateModified` TEXT NOT NULL,
`dateModified` TEXT NOT NULL, hash TEXT DEFAULT "" NOT NULL, dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z',
PRIMARY KEY(`branchId`)
);
CREATE INDEX `IDX_branches_noteId` ON `branches` (
@@ -109,12 +82,6 @@ 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,
@@ -125,8 +92,61 @@ CREATE TABLE labels
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
);
, hash TEXT DEFAULT "" 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 "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', hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`noteId`)
);
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
CREATE INDEX IDX_notes_type
on notes (type);
CREATE TABLE IF NOT EXISTS "recent_notes" (
`branchId` TEXT NOT NULL PRIMARY KEY,
`notePath` TEXT NOT NULL,
hash TEXT DEFAULT "" NOT NULL,
`dateCreated` TEXT NOT NULL,
isDeleted INT
);
CREATE TABLE IF NOT EXISTS "event_log" (
`eventId` TEXT NOT NULL PRIMARY KEY,
`noteId` TEXT,
`comment` TEXT,
`dateCreated` TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "options"
(
name TEXT not null PRIMARY KEY,
value TEXT,
dateModified INT,
isSynced INTEGER default 0 not null,
hash TEXT default "" not null,
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
);
CREATE TABLE relations
(
relationId TEXT not null primary key,
sourceNoteId TEXT not null,
name TEXT not null,
targetNoteId TEXT not null,
isInheritable int DEFAULT 0 NULL,
position INT default 0 not null,
dateCreated TEXT not null,
dateModified TEXT not null,
isDeleted INT not null
, hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX IDX_relation_sourceNoteId
on relations (sourceNoteId);
CREATE INDEX IDX_relation_targetNoteId
on relations (targetNoteId);

5449
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,22 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.11.0-beta",
"version": "0.18.0",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
"trilium": "./src/www"
},
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
},
"scripts": {
"start": "node ./bin/www",
"start": "node ./src/www",
"test-electron": "xo",
"rebuild-electron": "electron-rebuild",
"start-electron": "electron . --disable-gpu",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
@@ -21,22 +24,20 @@
},
"dependencies": {
"async-mutex": "^0.1.3",
"axios": "^0.17.1",
"body-parser": "~1.18.2",
"axios": "^0.18",
"body-parser": "^1.18.3",
"cls-hooked": "^4.2.2",
"cookie-parser": "~1.4.3",
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.5.7",
"electron": "^2.0.0-beta.5",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"electron-rebuild": "^1.7.3",
"ejs": "~2.6.1",
"electron-debug": "^2.0.0",
"electron-dl": "^1.12.0",
"electron-in-page-search": "^1.3.2",
"express": "~4.16.3",
"express-session": "^1.15.6",
"fs-extra": "^4.0.3",
"helmet": "^3.12.0",
"fs-extra": "^6.0.1",
"helmet": "^3.12.1",
"html": "^1.0.0",
"image-type": "^3.0.0",
"imagemin": "^5.3.1",
@@ -45,30 +46,35 @@
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.21.0",
"multer": "^1.3.0",
"moment": "^2.22.2",
"multer": "^1.3.1",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.85.0",
"rcedit": "^1.1.0",
"request": "^2.87.0",
"request-promise": "^4.2.2",
"rimraf": "^2.6.2",
"sanitize-filename": "^1.6.1",
"scrypt": "^6.0.3",
"serve-favicon": "~2.4.5",
"serve-favicon": "~2.5.0",
"session-file-store": "^1.2.0",
"simple-node-logger": "^0.93.37",
"sqlite": "^2.9.1",
"tar-stream": "^1.5.5",
"sqlite": "^2.9.2",
"tar-stream": "^1.6.1",
"unescape": "^1.0.1",
"ws": "^3.3.3"
"ws": "^5.2.1",
"xml2js": "^0.4.19"
},
"devDependencies": {
"electron-compile": "^6.4.2",
"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"
"electron": "^2.0.4",
"electron-compile": "^6.4.3",
"electron-packager": "^12.1.0",
"electron-prebuilt-compile": "2.0.4",
"electron-rebuild": "^1.8.1",
"lorem-ipsum": "^1.0.5",
"tape": "^4.9.1",
"xo": "^0.21.1",
"pkg": "^4.3.3"
},
"config": {
"forge": {
@@ -107,5 +113,15 @@
"node",
"browser"
]
},
"pkg": {
"assets": [
"./db/**/*",
"./src/public/**/*",
"./src/views/**/*",
"./node_modules/mozjpeg/vendor/*",
"./node_modules/pngquant-bin/vendor/*",
"./node_modules/giflossy/vendor/*"
]
}
}

View File

@@ -47,7 +47,7 @@ const sessionParser = session({
cookie: {
// path: "/",
httpOnly: true,
maxAge: 1800000
maxAge: 24 * 60 * 60 * 1000 // in milliseconds
},
store: new FileStore({
ttl: 30 * 24 * 3600,

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class ApiToken extends Entity {
static get tableName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
beforeSaving() {
super.beforeSaving();

View File

@@ -8,6 +8,8 @@ const sql = require('../services/sql');
class Branch extends Entity {
static get tableName() { return "branches"; }
static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
@@ -25,7 +27,11 @@ class Branch extends Entity {
this.isDeleted = false;
}
this.dateModified = dateUtils.nowDate()
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}

View File

@@ -1,7 +1,6 @@
"use strict";
const utils = require('../services/utils');
const repository = require('../services/repository');
class Entity {
constructor(row = {}) {
@@ -14,10 +13,18 @@ class Entity {
if (!this[this.constructor.primaryKeyName]) {
this[this.constructor.primaryKeyName] = utils.newEntityId();
}
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += "|" + this[propertyName];
}
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
async save() {
await repository.updateEntity(this);
await require('../services/repository').updateEntity(this);
return this;
}

View File

@@ -4,8 +4,10 @@ const Image = require('../entities/image');
const NoteImage = require('../entities/note_image');
const Branch = require('../entities/branch');
const Label = require('../entities/label');
const Relation = require('../entities/relation');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository');
function createEntityFromRow(row) {
@@ -14,6 +16,9 @@ function createEntityFromRow(row) {
if (row.labelId) {
entity = new Label(row);
}
else if (row.relationId) {
entity = new Relation(row);
}
else if (row.noteRevisionId) {
entity = new NoteRevision(row);
}
@@ -35,6 +40,9 @@ function createEntityFromRow(row) {
else if (row.noteId) {
entity = new Note(row);
}
else if (row.name) {
entity = new Option(row);
}
else {
throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
}

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class Image extends Entity {
static get tableName() { return "images"; }
static get primaryKeyName() { return "imageId"; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
beforeSaving() {
super.beforeSaving();

View File

@@ -8,6 +8,7 @@ const sql = require('../services/sql');
class Label extends Entity {
static get tableName() { return "labels"; }
static get primaryKeyName() { return "labelId"; }
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,33 +1,33 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const protectedSessionService = 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"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
constructor(row) {
super(row);
// 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);
protectedSessionService.decryptNote(this);
}
if (this.isJson()) {
this.jsonContent = JSON.parse(this.content);
}
this.setContent(this.content);
}
setContent(content) {
this.content = content;
if (this.isJson()) {
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
isJson() {
@@ -40,7 +40,7 @@ class Note extends Entity {
}
isHtml() {
return (this.type === "code" || this.type === "file") && this.mime === "text/html";
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
}
getScriptEnv() {
@@ -142,12 +142,12 @@ class Note extends Entity {
beforeSaving() {
super.beforeSaving();
if (this.isJson()) {
if (this.isJson() && this.jsonContent) {
this.content = JSON.stringify(this.jsonContent, null, '\t');
}
if (this.isProtected) {
protected_session.encryptNote(this);
protectedSessionService.encryptNote(this);
}
if (!this.isDeleted) {

View File

@@ -7,6 +7,7 @@ const dateUtils = require('../services/date_utils');
class NoteImage extends Entity {
static get tableName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; }
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,19 +1,19 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const utils = require('../services/utils');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNoteRevision(this);
protectedSessionService.decryptNoteRevision(this);
}
}
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
super.beforeSaving();
if (this.isProtected) {
protected_session.encryptNoteRevision(this);
protectedSessionService.encryptNoteRevision(this);
}
}
}

18
src/entities/option.js Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class Option extends Entity {
static get tableName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
beforeSaving() {
super.beforeSaving();
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Option;

View File

@@ -1,10 +1,24 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class RecentNote extends Entity {
static get tableName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateCreated", "isDeleted"]; }
beforeSaving() {
super.beforeSaving();
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
}
}
module.exports = RecentNote;

44
src/entities/relation.js Normal file
View File

@@ -0,0 +1,44 @@
"use strict";
const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils');
const sql = require('../services/sql');
class Relation extends Entity {
static get tableName() { return "relations"; }
static get primaryKeyName() { return "relationId"; }
static get hashedProperties() { return ["relationId", "sourceNoteId", "name", "targetNoteId", "isInheritable", "dateModified", "dateCreated"]; }
async getSourceNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.sourceNoteId]);
}
async getTargetNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.targetNoteId]);
}
async beforeSaving() {
super.beforeSaving();
if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM relations WHERE sourceNoteId = ?`, [this.sourceNoteId]);
}
if (!this.isInheritable) {
this.isInheritable = false;
}
if (!this.isDeleted) {
this.isDeleted = false;
}
if (!this.dateCreated) {
this.dateCreated = dateUtils.nowDate();
}
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Relation;

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

View File

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

View File

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 288 B

After

Width:  |  Height:  |  Size: 288 B

View File

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

View File

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View File

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

View File

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M63.966,45.043c0.008-0.009,0.021-0.021,0.027-0.029c0.938-1.156-0.823-13.453-5.063-20.125 c-1.389-2.186-2.239-3.423-3.219-4.719c-3.907-5.166-6-6.125-6-6.125S35.732,24.78,36.149,44.315 c-1.754,0.065-11.218,7.528-14.826,14.388c-1.206,2.291-1.856,3.645-2.493,5.141c-2.539,5.957-2.33,8.25-2.33,8.25 s16.271,6.79,33.014-3.294c0.007,0.021,0.013,0.046,0.02,0.063c0.537,1.389,12.08,5.979,19.976,5.621 c2.587-0.116,4.084-0.238,5.696-0.444c6.424-0.818,8.298-2.157,8.298-2.157S81.144,54.396,63.966,45.043z M50.787,65.343 c1.059-1.183,4.648-5.853,0.995-11.315c-0.253-0.377-0.496-0.236-0.496-0.236s0.063,10.822-5.162,12.359 c-5.225,1.537-13.886,4.4-20.427,0.455C25,66.186,26.924,53.606,38.544,47.229c0.546,1.599,2.836,6.854,9.292,6.409 c0.453-0.031,0.453-0.313,0.453-0.313s-9.422-5.328-8.156-10.625s3.089-14.236,9.766-17.948c0.714-0.397,10.746,7.593,10.417,20.94 c-1.606-0.319-7.377-1.004-10.226,4.864c-0.198,0.409,0.046,0.549,0.046,0.549s9.31-5.521,13.275-1.789 c3.965,3.733,10.813,9.763,10.71,17.4C74.111,67.533,62.197,72.258,50.787,65.343z M35.613,35.145c0,0-0.991,3.241-0.603,7.524 l-13.393-7.524C21.618,35.145,27.838,30.931,35.613,35.145z M21.193,36.03l13.344,7.612c-3.872,1.872-6.142,4.388-6.142,4.388 C20.78,43.531,21.193,36.03,21.193,36.03z M72.287,49.064c0,0-2.321-2.471-6.23-4.263l13.187-7.881 C79.243,36.92,79.808,44.413,72.287,49.064z M78.687,36.113l-13.237,7.794c0.3-4.291-0.754-7.511-0.754-7.511 C72.383,32.025,78.687,36.113,78.687,36.113z M42.076,73.778c0,0,3.309-0.737,6.845-3.185l0.056,15.361 C48.977,85.955,42.244,82.621,42.076,73.778z M49.956,85.888L50,70.526c3.539,2.445,6.846,3.181,6.846,3.181 C56.686,82.551,49.956,85.888,49.956,85.888z"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -2,7 +2,8 @@ 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';
import server from "../services/server.js";
import noteDetailText from "../services/note_detail_text.js";
const $dialog = $("#add-link-dialog");
const $form = $("#add-link-form");
@@ -11,8 +12,10 @@ const $linkTitle = $("#link-title");
const $clonePrefix = $("#clone-prefix");
const $linkTitleFormGroup = $("#add-link-title-form-group");
const $prefixFormGroup = $("#add-link-prefix-form-group");
const $linkTypeDiv = $("#add-link-type-div");
const $linkTypes = $("input[name='add-link-type']");
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
const $showRecentNotesButton = $("#add-link-show-recent-notes");
function setLinkType(linkType) {
$linkTypes.each(function () {
@@ -45,18 +48,39 @@ async function showDialog() {
$clonePrefix.val('');
$linkTitle.val('');
function setDefaultLinkTitle(noteId) {
const noteTitle = treeUtils.getNoteTitle(noteId);
async function setDefaultLinkTitle(noteId) {
const noteTitle = await treeUtils.getNoteTitle(noteId);
$linkTitle.val(noteTitle);
}
$autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(),
await $autoComplete.autocomplete({
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
if (result.length > 0) {
response(result.map(row => {
return {
label: row.label,
value: row.label + ' (' + row.value + ')'
}
}));
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
minLength: 0,
change: () => {
const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val);
change: async (event, ui) => {
if (!ui.item) {
return;
}
const notePath = linkService.getNotePathFromLabel(ui.item.value);
if (!notePath) {
return;
}
@@ -64,24 +88,33 @@ async function showDialog() {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (noteId) {
setDefaultLinkTitle(noteId);
await setDefaultLinkTitle(noteId);
}
},
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
},
// 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 = linkService.getNodePathFromLabel(ui.item.value);
focus: async (event, ui) => {
const notePath = linkService.getNotePathFromLabel(ui.item.value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
setDefaultLinkTitle(noteId);
await setDefaultLinkTitle(noteId);
event.preventDefault();
}
});
showRecentNotes();
}
$form.submit(() => {
const value = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(value);
const notePath = linkService.getNotePathFromLabel(value);
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
if (notePath) {
@@ -92,7 +125,16 @@ $form.submit(() => {
$dialog.dialog("close");
linkService.addLinkToEditor(linkTitle, '#' + notePath);
const linkHref = '#' + notePath;
if (hasSelection()) {
const editor = noteDetailText.getEditor();
editor.execute('link', linkHref);
}
else {
linkService.addLinkToEditor(linkTitle, linkHref);
}
}
else if (linkType === 'selected-to-current') {
const prefix = $clonePrefix.val();
@@ -113,21 +155,31 @@ $form.submit(() => {
return false;
});
// returns true if user selected some text, false if there's no selection
function hasSelection() {
const model = noteDetailText.getEditor().model;
const selection = model.document.selection;
return !selection.isCollapsed;
}
function linkTypeChanged() {
const value = $linkTypes.filter(":checked").val();
if (value === 'html') {
$linkTitleFormGroup.show();
$prefixFormGroup.hide();
}
else {
$linkTitleFormGroup.hide();
$prefixFormGroup.show();
}
$linkTitleFormGroup.toggle(!hasSelection() && value === 'html');
$prefixFormGroup.toggle(!hasSelection() && value !== 'html');
$linkTypeDiv.toggle(!hasSelection());
}
function showRecentNotes() {
$autoComplete.autocomplete("search", "");
}
$linkTypes.change(linkTypeChanged);
$showRecentNotesButton.click(showRecentNotes);
export default {
showDialog
};

View File

@@ -25,7 +25,7 @@ async function showDialog() {
$treePrefixInput.val(branch.prefix).focus();
const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle);
}

View File

@@ -19,10 +19,10 @@ async function showDialog() {
$list.html('');
for (const event of result) {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
const dateTime = utils.formatDateTime(utils.parseDate(event.dateCreated));
if (event.noteId) {
const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
event.comment = event.comment.replace('<note>', noteLink);
}

View File

@@ -1,11 +1,11 @@
import treeService from '../services/tree.js';
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import autocompleteService from '../services/autocomplete.js';
import server from '../services/server.js';
import searchNotesService from '../services/search_notes.js';
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form");
const $showInFullTextButton = $("#show-in-full-text-button");
const $showRecentNotesButton = $("#jump-to-note-show-recent-notes");
async function showDialog() {
glob.activeDialog = $dialog;
@@ -14,35 +14,66 @@ async function showDialog() {
$dialog.dialog({
modal: true,
width: 800
width: 800,
position: { my: "center top+100", at: "top", of: window }
});
await $autoComplete.autocomplete({
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
minLength: 1
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
if (result.length > 0) {
response(result);
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
focus: function(event, ui) {
event.preventDefault();
},
minLength: 0,
autoFocus: true,
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
treeService.activateNode(ui.item.value);
$dialog.dialog('close');
}
});
showRecentNotes();
}
function getSelectedNotePath() {
const val = $autoComplete.val();
return linkService.getNodePathFromLabel(val);
function showInFullText(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
const searchText = $autoComplete.val();
searchNotesService.resetSearch();
searchNotesService.showSearch();
searchNotesService.doSearch(searchText);
$dialog.dialog('close');
}
function goToNote() {
const notePath = getSelectedNotePath();
if (notePath) {
treeService.activateNode(notePath);
$dialog.dialog('close');
}
function showRecentNotes() {
$autoComplete.autocomplete("search", "");
}
$form.submit(() => {
goToNote();
$showInFullTextButton.click(showInFullText);
return false;
});
$showRecentNotesButton.click(showRecentNotes);
$dialog.bind('keydown', 'ctrl+return', showInFullText);
export default {
showDialog

View File

@@ -1,5 +1,4 @@
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";
@@ -168,9 +167,9 @@ async function showDialog() {
});
}
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
ko.applyBindings(labelsModel, $dialog[0]);
$(document).on('focus', '.label-name', function (e) {
$dialog.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
@@ -188,7 +187,7 @@ $(document).on('focus', '.label-name', function (e) {
$(this).autocomplete("search", $(this).val());
});
$(document).on('focus', '.label-value', async function (e) {
$dialog.on('focus', '.label-value', async function (e) {
if (!$(this).hasClass("ui-autocomplete-input")) {
const labelName = $(this).parent().parent().find('.label-name').val();

View File

@@ -29,7 +29,7 @@ function formatNode(node, level) {
const indentAfter = new Array(level - 1).join(' ');
let textNode;
for (const i = 0; i < node.children.length; i++) {
for (let i = 0; i < node.children.length; i++) {
textNode = document.createTextNode('\n' + indentBefore);
node.insertBefore(textNode, node.children[i]);

View File

@@ -1,9 +1,10 @@
"use strict";
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";
import zoomService from "../services/zoom.js";
import utils from "../services/utils.js";
const $dialog = $("#options-dialog");
const $tabs = $("#options-tabs");
@@ -33,8 +34,8 @@ async function showDialog() {
}
}
async function saveOptions(optionName, optionValue) {
await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
async function saveOptions(options) {
await server.put('options', options);
infoService.showMessage("Options change have been saved.");
}
@@ -44,6 +45,41 @@ export default {
saveOptions
};
addTabHandler((function() {
const $themeSelect = $("#theme-select");
const $zoomFactorSelect = $("#zoom-factor-select");
const $html = $("html");
function optionsLoaded(options) {
$themeSelect.val(options.theme);
if (utils.isElectron()) {
$zoomFactorSelect.val(options.zoomFactor);
}
else {
$zoomFactorSelect.prop('disabled', true);
}
}
$themeSelect.change(function() {
const newTheme = $(this).val();
$html.attr("class", "theme-" + newTheme);
server.put('options/theme/' + newTheme);
});
$zoomFactorSelect.change(function() {
const newZoomFactor = $(this).val();
zoomService.setZoomFactorAndSave(newZoomFactor);
});
return {
optionsLoaded
};
})());
addTabHandler((function() {
const $form = $("#change-password-form");
const $oldPassword = $("#old-password");
@@ -93,16 +129,15 @@ addTabHandler((function() {
addTabHandler((function() {
const $form = $("#protected-session-timeout-form");
const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
const optionName = 'protectedSessionTimeout';
function optionsLoaded(options) {
$protectedSessionTimeout.val(options[optionName]);
$protectedSessionTimeout.val(options['protectedSessionTimeout']);
}
$form.submit(() => {
const protectedSessionTimeout = $protectedSessionTimeout.val();
saveOptions(optionName, protectedSessionTimeout).then(() => {
saveOptions({ 'protectedSessionTimeout': protectedSessionTimeout }).then(() => {
protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
});
@@ -117,14 +152,13 @@ addTabHandler((function() {
addTabHandler((function () {
const $form = $("#note-revision-snapshot-time-interval-form");
const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
const optionName = 'noteRevisionSnapshotTimeInterval';
function optionsLoaded(options) {
$timeInterval.val(options[optionName]);
$timeInterval.val(options['noteRevisionSnapshotTimeInterval']);
}
$form.submit(() => {
saveOptions(optionName, $timeInterval.val());
saveOptions({ 'noteRevisionSnapshotTimeInterval': $timeInterval.val() });
return false;
});
@@ -137,6 +171,7 @@ addTabHandler((function () {
addTabHandler((async function () {
const $appVersion = $("#app-version");
const $dbVersion = $("#db-version");
const $syncVersion = $("#sync-version");
const $buildDate = $("#build-date");
const $buildRevision = $("#build-revision");
@@ -144,6 +179,7 @@ addTabHandler((async function () {
$appVersion.html(appInfo.appVersion);
$dbVersion.html(appInfo.dbVersion);
$syncVersion.html(appInfo.syncVersion);
$buildDate.html(appInfo.buildDate);
$buildRevision.html(appInfo.buildRevision);
$buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
@@ -151,6 +187,57 @@ addTabHandler((async function () {
return {};
})());
addTabHandler((function() {
const $form = $("#sync-setup-form");
const $syncServerHost = $("#sync-server-host");
const $syncServerTimeout = $("#sync-server-timeout");
const $syncProxy = $("#sync-proxy");
const $testSyncButton = $("#test-sync-button");
const $syncToServerButton = $("#sync-to-server-button");
function optionsLoaded(options) {
$syncServerHost.val(options['syncServerHost']);
$syncServerTimeout.val(options['syncServerTimeout']);
$syncProxy.val(options['syncProxy']);
}
$form.submit(() => {
saveOptions({
'syncServerHost': $syncServerHost.val(),
'syncServerTimeout': $syncServerTimeout.val(),
'syncProxy': $syncProxy.val()
});
return false;
});
$testSyncButton.click(async () => {
const result = await server.post('sync/test');
if (result.connection === "Success") {
infoService.showMessage("Sync server handshake has been successful");
}
else {
infoService.showError("Sync server handshake failed, error: " + result.error);
}
});
$syncToServerButton.click(async () => {
const resp = await server.post("setup/sync-to-server");
if (resp.success) {
infoService.showMessage("Sync has been established to the server instance. It will take some time to finish.");
}
else {
infoService.showError('Sync setup failed: ' + resp.error);
}
});
return {
optionsLoaded
};
})());
addTabHandler((async function () {
const $forceFullSyncButton = $("#force-full-sync-button");
const $fillSyncRowsButton = $("#fill-sync-rows-button");

View File

@@ -40,7 +40,7 @@ async function showDialog() {
noteLink = change.current_title;
}
else {
noteLink = linkService.createNoteLink(change.noteId, change.title);
noteLink = await linkService.createNoteLink(change.noteId, change.title);
}
changesListEl.append($('<li>')

View File

@@ -1,113 +0,0 @@
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 $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input');
// list of recent note paths
let list = [];
async function reload() {
const result = await server.get('recent-notes');
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);
}
return noteTitle;
}
async 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 !== 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

@@ -0,0 +1,250 @@
import noteDetailService from '../services/note_detail.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
import linkService from "../services/link.js";
import treeUtils from "../services/tree_utils.js";
const $dialog = $("#relations-dialog");
const $saveRelationsButton = $("#save-relations-button");
const $relationsBody = $('#relations-table tbody');
const relationsModel = new RelationsModel();
let relationNames = [];
function RelationsModel() {
const self = this;
this.relations = ko.observableArray();
this.updateRelationPositions = function() {
let position = 0;
// we need to update positions by searching in the DOM, because order of the
// relations in the viewmodel (self.relations()) stays the same
$relationsBody.find('input[name="position"]').each(function() {
const relation = self.getTargetRelation(this);
relation().position = position++;
});
};
async function showRelations(relations) {
for (const relation of relations) {
relation.targetNoteId = await treeUtils.getNoteTitle(relation.targetNoteId) + " (" + relation.targetNoteId + ")";
}
self.relations(relations.map(ko.observable));
}
this.loadRelations = async function() {
const noteId = noteDetailService.getCurrentNoteId();
const relations = await server.get('notes/' + noteId + '/relations');
await showRelations(relations);
addLastEmptyRow();
relationNames = await server.get('relations/names');
// relation might not be rendered immediatelly so could not focus
setTimeout(() => $(".relation-name:last").focus(), 100);
$relationsBody.sortable({
handle: '.handle',
containment: $relationsBody,
update: this.updateRelationPositions
});
};
this.deleteRelation = function(data, event) {
const relation = self.getTargetRelation(event.target);
const relationData = relation();
if (relationData) {
relationData.isDeleted = 1;
relation(relationData);
addLastEmptyRow();
}
};
function isValid() {
for (let relations = self.relations(), i = 0; i < relations.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.
$saveRelationsButton.focus();
if (!isValid()) {
alert("Please fix all validation errors and try saving again.");
return;
}
self.updateRelationPositions();
const noteId = noteDetailService.getCurrentNoteId();
const relationsToSave = self.relations()
.map(relation => relation())
.filter(relation => relation.relationId !== "" || relation.name !== "");
relationsToSave.forEach(relation => relation.targetNoteId = treeUtils.getNoteIdFromNotePath(linkService.getNotePathFromLabel(relation.targetNoteId)));
console.log(relationsToSave);
const relations = await server.put('notes/' + noteId + '/relations', relationsToSave);
await showRelations(relations);
addLastEmptyRow();
infoService.showMessage("Relations have been saved.");
noteDetailService.loadRelationList();
};
function addLastEmptyRow() {
const relations = self.relations().filter(attr => attr().isDeleted === 0);
const last = relations.length === 0 ? null : relations[relations.length - 1]();
if (!last || last.name.trim() !== "" || last.targetNoteId !== "") {
self.relations.push(ko.observable({
relationId: '',
name: '',
targetNoteId: '',
isInheritable: 0,
isDeleted: 0,
position: 0
}));
}
}
this.relationChanged = function (data, event) {
addLastEmptyRow();
const relation = self.getTargetRelation(event.target);
relation.valueHasMutated();
};
this.isNotUnique = function(index) {
const cur = self.relations()[index]();
if (cur.name.trim() === "") {
return false;
}
for (let relations = self.relations(), i = 0; i < relations.length; i++) {
const relation = relations[i]();
if (index !== i && cur.name === relation.name) {
return true;
}
}
return false;
};
this.isEmptyName = function(index) {
const cur = self.relations()[index]();
return cur.name.trim() === "" && (cur.relationId !== "" || cur.targetNoteId !== "");
};
this.getTargetRelation = function(target) {
const context = ko.contextFor(target);
const index = context.$index();
return self.relations()[index];
}
}
async function showDialog() {
glob.activeDialog = $dialog;
await relationsModel.loadRelations();
$dialog.dialog({
modal: true,
width: 900,
height: 500
});
}
ko.applyBindings(relationsModel, document.getElementById('relations-dialog'));
$dialog.on('focus', '.relation-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: relationNames.map(relation => {
return {
label: relation,
value: relation
}
}),
minLength: 0
});
}
$(this).autocomplete("search", $(this).val());
});
async function initNoteAutocomplete($el) {
if (!$el.hasClass("ui-autocomplete-input")) {
await $el.autocomplete({
source: async function (request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
if (result.length > 0) {
response(result.map(row => {
return {
label: row.label,
value: row.label + ' (' + row.value + ')'
}
}));
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
minLength: 0,
select: function (event, ui) {
if (ui.item.value === 'No results') {
return false;
}
}
});
}
}
$dialog.on('focus', '.relation-target-note-id', async function () {
await initNoteAutocomplete($(this));
});
$dialog.on('click', '.relations-show-recent-notes', async function () {
const $autocomplete = $(this).parent().find('.relation-target-note-id');
await initNoteAutocomplete($autocomplete);
$autocomplete.autocomplete("search", "");
});
export default {
showDialog
};

View File

@@ -6,8 +6,11 @@ class NoteFull extends NoteShort {
this.content = row.content;
if (this.isJson()) {
this.jsonContent = JSON.parse(this.content);
if (this.content !== "" && this.isJson()) {
try {
this.jsonContent = JSON.parse(this.content);
}
catch(e) {}
}
}
}

View File

@@ -6,7 +6,7 @@ class NoteShort {
this.isProtected = row.isProtected;
this.type = row.type;
this.mime = row.mime;
this.hideInAutocomplete = row.hideInAutocomplete;
this.archived = row.archived;
}
isJson() {
@@ -14,33 +14,44 @@ class NoteShort {
}
async getBranches() {
const branches = [];
const branchIds = this.treeCache.parents[this.noteId].map(
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
for (const parent of this.treeCache.parents[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
}
return this.treeCache.getBranches(branchIds);
}
return branches;
hasChildren() {
return this.treeCache.children[this.noteId]
&& this.treeCache.children[this.noteId].length > 0;
}
async getChildBranches() {
const branches = [];
for (const child of this.treeCache.children[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
if (!this.treeCache.children[this.noteId]) {
return [];
}
return branches;
const branchIds = this.treeCache.children[this.noteId].map(
childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
return await this.treeCache.getBranches(branchIds);
}
async getParentNotes() {
getParentNoteIds() {
return this.treeCache.parents[this.noteId] || [];
}
async getChildNotes() {
async getParentNotes() {
return await this.treeCache.getNotes(this.getParentNoteIds());
}
getChildNoteIds() {
return this.treeCache.children[this.noteId] || [];
}
async getChildNotes() {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`;
}
@@ -48,7 +59,7 @@ class NoteShort {
get dto() {
const dto = Object.assign({}, this);
delete dto.treeCache;
delete dto.hideInAutocomplete;
delete dto.archived;
return dto;
}

View File

@@ -1,46 +0,0 @@
import server from './services/server.js';
$(document).ready(async () => {
const {appDbVersion, dbVersion} = await server.get('migration');
console.log("HI", {appDbVersion, 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 () => {
$("#run-migration").prop("disabled", true);
$("#migration-result").show();
const result = await server.post('migration');
for (const migration of result.migrations) {
const row = $('<tr>')
.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));
if (!migration.success) {
row.addClass("danger");
}
$("#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,104 +0,0 @@
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

@@ -4,7 +4,6 @@ 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';
@@ -17,7 +16,7 @@ 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 searchNotesService from './search_notes.js';
import ScriptApi from './script_api.js';
import ScriptContext from './script_context.js';
import sync from './sync.js';
@@ -35,6 +34,7 @@ import libraryLoader from "./library_loader.js";
// required for CKEditor image upload plugin
window.glob.getCurrentNode = treeService.getCurrentNode;
window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = addLinkDialog.showDialog;
// required for ESLint plugin
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
@@ -46,7 +46,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){
if (string.includes("Cannot read property 'defaultView' of undefined")) {
// ignore this specific error which is very common but we don't know where it comes from
// and it seems to be harmless
return true;
}
else if (string.includes("script error")) {
message += 'No details available';
}
else {

View File

@@ -1,8 +1,14 @@
import ScriptContext from "./script_context.js";
import server from "./server.js";
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
async function getAndExecuteBundle(noteId, targetNote = null) {
const bundle = await server.get('script/bundle/' + noteId);
await executeBundle(bundle, targetNote);
}
async function executeBundle(bundle, targetNote) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes, targetNote);
return await (function () {
return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
@@ -17,7 +23,17 @@ async function executeStartupBundles() {
}
}
async function executeRelationBundles(note, relationName) {
const bundlesToRun = await server.get("script/relation/" + note.noteId + "/" + relationName);
for (const bundle of bundlesToRun) {
await executeBundle(bundle, note);
}
}
export default {
executeBundle,
executeStartupBundles
getAndExecuteBundle,
executeStartupBundles,
executeRelationBundles
}

View File

@@ -94,27 +94,34 @@ const contextMenuOptions = {
{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: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne", children: [
{title: "Native&nbsp;Tar", cmd: "exportBranchToTar"},
{title: "OPML", cmd: "exportBranchToOpml"}
]},
{title: "Import into branch (tar, opml)", 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);
const isNotRoot = note.noteId !== 'root';
// 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", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "delete", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "copy", isNotRoot);
$tree.contextmenu("enableEntry", "cut", isNotRoot);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "editBranchPrefix", isNotRoot && parentNote.type !== 'search');
// Activate node on right-click
node.setActive();
@@ -159,8 +166,11 @@ const contextMenuOptions = {
else if (ui.cmd === "delete") {
treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
}
else if (ui.cmd === "exportBranch") {
exportService.exportBranch(node.data.noteId);
else if (ui.cmd === "exportBranchToTar") {
exportService.exportBranch(node.data.noteId, 'tar');
}
else if (ui.cmd === "exportBranchToOpml") {
exportService.exportBranch(node.data.noteId, 'opml');
}
else if (ui.cmd === "importBranch") {
exportService.importBranch(node.data.noteId);

View File

@@ -2,16 +2,18 @@ import utils from "./utils.js";
import treeService from "./tree.js";
import linkService from "./link.js";
import fileService from "./file.js";
import zoomService from "./zoom.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 searchNotesService from "./search_notes.js";
import labelsDialog from "../dialogs/labels.js";
import relationsDialog from "../dialogs/relations.js";
import protectedSessionService from "./protected_session.js";
function registerEntrypoints() {
// hot keys are active also inside inputs and content editables
@@ -21,35 +23,47 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
$("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
$("#jump-to-note-dialog-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);
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
$("#toggle-search-button").click(searchTreeService.toggleSearch);
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
$("#toggle-search-button").click(searchNotesService.toggleSearch);
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
$(".show-labels-button").click(labelsDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog);
$(".show-relations-button").click(relationsDialog.showDialog);
utils.bindShortcut('alt+r', relationsDialog.showDialog);
$("#options-button").click(optionsDialog.showDialog);
utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
if (utils.isElectron()) {
$("#history-navigation").show();
$("#history-back-button").click(window.history.back);
$("#history-forward-button").click(window.history.forward);
utils.bindShortcut('alt+left', window.history.back);
utils.bindShortcut('alt+right', window.history.forward);
}
utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
utils.bindShortcut('alt+m', e => {
$(".hide-toggle").toggle();
// when hiding switch display to block, otherwise grid still tries to display columns which shows
// left empty column
$("#container").css("display", $("#container").css("display") === "grid" ? "block" : "grid");
});
// hide (toggle) everything except for the note content for distraction free writing
utils.bindShortcut('alt+t', e => {
@@ -101,27 +115,10 @@ function registerEntrypoints() {
$("#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;
}
});
if (utils.isElectron()) {
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
}
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

@@ -3,9 +3,9 @@ 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());
function exportBranch(noteId, format) {
const url = utils.getHost() + "/api/notes/" + noteId + "/export/" + format +
"?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
utils.download(url);
}
@@ -29,7 +29,7 @@ $("#import-upload").change(async function() {
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
}).fail((xhr, status, error) => alert('Import error: ' + xhr.responseText));
await treeService.reload();
});

View File

@@ -13,7 +13,7 @@ function getNotePathFromLink(url) {
}
}
function getNodePathFromLabel(label) {
function getNotePathFromLabel(label) {
const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
if (notePathMatch !== null) {
@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
return null;
}
function createNoteLink(notePath, noteTitle) {
async function createNoteLink(notePath, noteTitle) {
if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeUtils.getNoteTitle(noteId);
noteTitle = await treeUtils.getNoteTitle(noteId);
}
const noteLink = $("<a>", {
@@ -76,9 +76,11 @@ function goToLink(e) {
function addLinkToEditor(linkTitle, linkHref) {
const editor = noteDetailText.getEditor();
const doc = editor.document;
doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
editor.model.change( writer => {
const insertPosition = editor.model.document.selection.getFirstPosition();
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
});
}
function addTextToEditor(text) {
@@ -95,7 +97,7 @@ $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToL
$(document).on('dblclick', '#note-detail-text a', goToLink);
export default {
getNodePathFromLabel,
getNotePathFromLabel,
getNotePathFromLink,
createNoteLink,
addLinkToEditor,

View File

@@ -1,7 +1,7 @@
import utils from './utils.js';
import infoService from "./info.js";
const $changesToPushCount = $("#changes-to-push-count");
const $outstandingSyncsCount = $("#outstanding-syncs-count");
const messageHandlers = [];
@@ -43,7 +43,7 @@ function handleMessage(event) {
messageHandler(syncData);
}
$changesToPushCount.html(message.changesToPushCount);
$outstandingSyncsCount.html(message.outstandingSyncs);
}
else if (message.type === 'sync-hash-check-failed') {
infoService.showError("Sync check failed!", 60000);

View File

@@ -7,6 +7,7 @@ import utils from './utils.js';
import server from './server.js';
import messagingService from "./messaging.js";
import infoService from "./info.js";
import linkService from "./link.js";
import treeCache from "./tree_cache.js";
import NoteFull from "../entities/note_full.js";
import noteDetailCode from './note_detail_code.js';
@@ -14,6 +15,7 @@ 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';
import bundleService from "./bundle.js";
const $noteTitle = $("#note-title");
@@ -22,10 +24,14 @@ const $noteDetailComponents = $(".note-detail-component");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner");
const $relationList = $("#relation-list");
const $relationListInner = $("#relation-list-inner");
const $childrenOverview = $("#children-overview");
const $scriptArea = $("#note-detail-script-area");
let currentNote = null;
@@ -116,9 +122,9 @@ async function saveNoteIfChanged() {
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
$noteDetailComponentWrapper.toggleClass("protected", isProtected);
$protectButton.toggleClass("active", isProtected);
$unprotectButton.toggleClass("active", !isProtected);
}
let isNewNoteCreated = false;
@@ -150,6 +156,8 @@ async function loadNoteDetail(noteId) {
$noteIdDisplay.html(noteId);
setNoteBackgroundIfProtected(currentNote);
await handleProtectedSession();
$noteDetailWrapper.show();
@@ -170,18 +178,29 @@ async function loadNoteDetail(noteId) {
noteChangeDisabled = false;
}
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
$noteDetailWrapper.scrollTop(0);
await loadLabelList();
const labels = await loadLabelList();
await showChildrenOverview();
const hideChildrenOverview = labels.some(label => label.name === 'hideChildrenOverview');
await showChildrenOverview(hideChildrenOverview);
await loadRelationList();
$scriptArea.html('');
await bundleService.executeRelationBundles(getCurrentNote(), 'runOnNoteView');
}
async function showChildrenOverview() {
async function showChildrenOverview(hideChildrenOverview) {
if (hideChildrenOverview) {
$childrenOverview.hide();
return;
}
const note = getCurrentNote();
$childrenOverview.empty();
@@ -197,6 +216,8 @@ async function showChildrenOverview() {
const childEl = $('<div class="child-overview">').html(link);
$childrenOverview.append(childEl);
}
$childrenOverview.show();
}
async function loadLabelList() {
@@ -216,6 +237,31 @@ async function loadLabelList() {
else {
$labelList.hide();
}
return labels;
}
async function loadRelationList() {
const noteId = getCurrentNoteId();
const relations = await server.get('notes/' + noteId + '/relations');
$relationListInner.html('');
if (relations.length > 0) {
for (const relation of relations) {
$relationListInner.append(relation.name + " = ");
$relationListInner.append(await linkService.createNoteLink(relation.targetNoteId));
$relationListInner.append(" ");
}
$relationList.show();
}
else {
$relationList.hide();
}
return relations;
}
async function loadNote(noteId) {
@@ -267,6 +313,7 @@ export default {
newNoteCreated,
focus,
loadLabelList,
loadRelationList,
saveNote,
saveNoteIfChanged,
noteChanged

View File

@@ -31,7 +31,8 @@ async function show() {
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true
lineNumbers: true,
tabindex: 2 // so that tab from title will lead to code editor focus
});
codeEditor.on('change', noteDetailService.noteChanged);
@@ -63,24 +64,25 @@ function 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");
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (noteDetailService.getCurrentNoteType() !== 'code') {
return;
}
// make sure note is saved so we load latest changes
await noteDetailService.saveNoteIfChanged();
const currentNote = noteDetailService.getCurrentNote();
if (currentNote.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(noteDetailService.getCurrentNoteId());
}
if (currentNote.mime.endsWith("env=backend")) {
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
}
infoService.showMessage("Note executed");
}
$(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,21 +1,73 @@
import bundleService from "./bundle.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
import noteDetailCodeService from "./note_detail_code.js";
const $noteDetailCode = $('#note-detail-code');
const $noteDetailRender = $('#note-detail-render');
const $toggleEditButton = $('#toggle-edit-button');
const $renderButton = $('#render-button');
let codeEditorInitialized;
async function show() {
codeEditorInitialized = false;
$noteDetailRender.show();
await render();
}
async function toggleEdit() {
if ($noteDetailCode.is(":visible")) {
$noteDetailCode.hide();
}
else {
if (!codeEditorInitialized) {
await noteDetailCodeService.show();
// because we can't properly scroll only the editor without scrolling the rendering
// we limit its height
$noteDetailCode.find('.CodeMirror').css('height', '300');
codeEditorInitialized = true;
}
else {
$noteDetailCode.show();
}
}
}
$toggleEditButton.click(toggleEdit);
$renderButton.click(render);
async function render() {
// ctrl+enter is also used elsewhere so make sure we're running only when appropriate
if (noteDetailService.getCurrentNoteType() !== 'render') {
return;
}
if (codeEditorInitialized) {
await noteDetailService.saveNoteIfChanged();
}
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
$noteDetailRender.html(bundle.html);
// if the note is empty, it doesn't make sense to do render-only since nothing will be rendered
if (!bundle.html.trim()) {
toggleEdit();
}
await bundleService.executeBundle(bundle);
}
$(document).bind('keydown', "ctrl+return", render);
export default {
show,
getContent: () => null,
getContent: noteDetailCodeService.getContent,
focus: () => null
}

View File

@@ -11,11 +11,16 @@ async function show() {
textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.document.on('change', noteDetailService.noteChanged);
textEditor.model.document.on('change', () => {
// change is triggered on just marker/selection changes which is not interesting for us
if (textEditor.model.document.differ.getChanges().length > 0) {
noteDetailService.noteChanged();
}
}
);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
textEditor.setData(noteDetailService.getCurrentNote().content);
$noteDetailText.show();
}

View File

@@ -4,6 +4,9 @@ import server from './server.js';
import infoService from "./info.js";
const $executeScriptButton = $("#execute-script-button");
const $toggleEditButton = $('#toggle-edit-button');
const $renderButton = $('#render-button');
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
@@ -107,7 +110,7 @@ function NoteTypeModel() {
this.selectRender = function() {
self.type('render');
self.mime('');
self.mime('text/html');
save();
};
@@ -128,6 +131,9 @@ function NoteTypeModel() {
this.updateExecuteScriptButtonVisibility = function() {
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
$toggleEditButton.toggle(self.type() === 'render');
$renderButton.toggle(self.type() === 'render');
}
}

View File

@@ -0,0 +1,9 @@
import server from "./server.js";
const optionsReady = new Promise((resolve, reject) => {
$(document).ready(() => server.get('options').then(resolve));
});
export default {
optionsReady
}

View File

@@ -11,9 +11,23 @@ const $password = $("#protected-session-password");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $protectedSessionOnButton = $("#protected-session-on");
const $protectedSessionOffButton = $("#protected-session-off");
let protectedSessionDeferred = null;
async function enterProtectedSession() {
if (!protectedSessionHolder.isProtectedSessionAvailable()) {
await ensureProtectedSession(true, true);
}
}
async function leaveProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
utils.reloadApp();
}
}
function ensureProtectedSession(requireProtectedSession, modal) {
const dfd = $.Deferred();
@@ -25,7 +39,10 @@ function ensureProtectedSession(requireProtectedSession, modal) {
}
$dialog.dialog({
modal: modal,
// modal: modal,
// everything is now non-modal, because modal dialog caused weird high CPU usage on opening
// and tearing of text input
modal: false,
width: 400,
open: () => {
if (!modal) {
@@ -46,7 +63,7 @@ async function setupProtectedSession() {
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
const response = await enterProtectedSessionOnServer(password);
if (!response.success) {
infoService.showError("Wrong password.");
@@ -66,8 +83,10 @@ async function setupProtectedSession() {
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
$protectedSessionOnButton.addClass('active');
$protectedSessionOffButton.removeClass('active');
}
}
@@ -81,13 +100,17 @@ function ensureDialogIsClosed() {
$password.val('');
}
async function enterProtectedSession(password) {
async function enterProtectedSessionOnServer(password) {
return await server.post('login/protected', {
password: password
});
}
async function protectNoteAndSendToServer() {
if (noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
@@ -101,6 +124,10 @@ async function protectNoteAndSendToServer() {
}
async function unprotectNoteAndSendToServer() {
if (!noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
@@ -138,5 +165,7 @@ export default {
protectNoteAndSendToServer,
unprotectNoteAndSendToServer,
protectBranch,
ensureDialogIsClosed
ensureDialogIsClosed,
enterProtectedSession,
leaveProtectedSession
};

View File

@@ -1,13 +1,11 @@
import utils from "./utils.js";
import server from "./server.js";
import optionsInitService from './options_init.js';
let lastProtectedSessionOperationDate = null;
let protectedSessionTimeout = null;
let protectedSessionId = null;
$(document).ready(() => {
server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
});
optionsInitService.optionsReady.then(options => protectedSessionTimeout = options.protectedSessionTimeout);
setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {

View File

@@ -2,8 +2,9 @@ import treeService from './tree.js';
import server from './server.js';
import utils from './utils.js';
import infoService from './info.js';
import linkService from './link.js';
function ScriptApi(startNote, currentNote) {
function ScriptApi(startNote, currentNote, targetNote = null) {
const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) {
@@ -42,7 +43,8 @@ function ScriptApi(startNote, currentNote) {
script: script,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId
currentNoteId: currentNote.noteId,
targetNoteId: targetNote ? targetNote.noteId : null
});
return ret.executionResult;
@@ -51,6 +53,7 @@ function ScriptApi(startNote, currentNote) {
return {
startNote: startNote,
currentNote: currentNote,
targetNote: targetNote,
addButtonToToolbar,
activateNote,
getInstanceName: () => window.glob.instanceName,
@@ -59,7 +62,8 @@ function ScriptApi(startNote, currentNote) {
parseDate: utils.parseDate,
showMessage: infoService.showMessage,
showError: infoService.showError,
reloadTree: treeService.reload
reloadTree: treeService.reload,
createNoteLink: linkService.createNoteLink
}
}

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