Compare commits

...

79 Commits

Author SHA1 Message Date
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
131 changed files with 7279 additions and 4529 deletions

4
.dockerignore Normal file
View File

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

View File

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

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
docker push zadam/trilium:latest
docker push zadam/trilium:$1

View File

@@ -75,4 +75,8 @@ github-release upload \
--name "$WINDOWS_X64_BUILD" \
--file "dist/$WINDOWS_X64_BUILD"
bin/build-docker.sh $VERSION
bin/push-docker-image.sh $VERSION
echo "Release finished!"

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

@@ -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,
@@ -29,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);
, 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`
);
@@ -49,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,
@@ -58,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);
@@ -68,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,
@@ -77,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` (
@@ -87,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,
@@ -103,18 +92,11 @@ 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 "event_log"
(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
noteId TEXT,
comment TEXT,
dateAdded TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed",
@@ -124,9 +106,31 @@ CREATE TABLE IF NOT EXISTS "notes" (
`dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text',
mime TEXT NOT NULL DEFAULT 'text/html',
mime TEXT NOT NULL DEFAULT 'text/html', hash TEXT DEFAULT "" NOT NULL,
PRIMARY KEY(`noteId`)
);
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
`isDeleted`
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
);

8509
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.12.0",
"version": "0.16.0",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -9,11 +9,11 @@
"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 +21,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",
"ejs": "~2.6.1",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"electron-rebuild": "^1.7.3",
"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 +43,34 @@
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.21.0",
"moment": "^2.22.1",
"multer": "^1.3.0",
"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.0",
"xml2js": "^0.4.19"
},
"devDependencies": {
"electron": "^2.0.1",
"electron-compile": "^6.4.2",
"electron-packager": "^11.1.0",
"electron-prebuilt-compile": "2.0.0-beta.5",
"electron-packager": "^12.1.0",
"electron-prebuilt-compile": "2.0.0",
"electron-rebuild": "^1.7.3",
"lorem-ipsum": "^1.0.4",
"tape": "^4.9.0",
"xo": "^0.18.0"
"xo": "^0.21.1"
},
"config": {
"forge": {

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

@@ -14,6 +14,14 @@ 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() {

View File

@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
const Label = require('../entities/label');
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) {
@@ -35,6 +36,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,20 +1,21 @@
"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);
}
this.setContent(this.content);
@@ -39,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() {
@@ -146,7 +147,7 @@ class Note extends Entity {
}
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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 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,6 +12,7 @@ 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"]');
@@ -52,8 +54,20 @@ async function showDialog() {
}
$autoComplete.autocomplete({
source: await autocompleteService.getAutocompleteItems(),
minLength: 0,
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"
}]);
}
},
minLength: 2,
change: async () => {
const val = $autoComplete.val();
const notePath = linkService.getNodePathFromLabel(val);
@@ -92,7 +106,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,17 +136,21 @@ $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());
}
$linkTypes.change(linkTypeChanged);

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,13 @@
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 $jumpToNoteButton = $("#jump-to-note-button");
const $showInFullTextButton = $("#show-in-full-text-button");
async function showDialog() {
glob.activeDialog = $dialog;
@@ -18,8 +20,23 @@ async function showDialog() {
});
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) {
return $(ui.item).val() !== 'No results';
},
minLength: 2
});
}
@@ -38,12 +55,32 @@ function goToNote() {
}
}
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');
}
$form.submit(() => {
goToNote();
return false;
});
$jumpToNoteButton.click(goToNote);
$showInFullTextButton.click(showInFullText);
$dialog.bind('keydown', 'ctrl+return', showInFullText);
export default {
showDialog
};

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");
@@ -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");
@@ -137,6 +173,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 +181,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);

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,47 +1,18 @@
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;
@@ -54,16 +25,17 @@ async function showDialog() {
$searchInput.val('');
// remove the current note
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
const items = [];
const result = await server.get('recent-notes');
for (const notePath of recNotes) {
items.push({
label: await getNoteTitle(notePath),
value: notePath
});
}
// remove the current note
const recNotes = result.filter(note => note.notePath !== treeService.getCurrentNotePath());
const items = recNotes.map(rn => {
return {
label: rn.title,
value: rn.notePath
};
});
$searchInput.autocomplete({
source: items,
@@ -96,18 +68,7 @@ async function showDialog() {
});
}
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
addRecentNote
};

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,13 +14,15 @@ 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() {
@@ -28,23 +30,28 @@ class NoteShort {
return [];
}
const branches = [];
const branchIds = this.treeCache.children[this.noteId].map(
childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
for (const child of this.treeCache.children[this.noteId]) {
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
}
return branches;
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})`;
}
@@ -52,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

@@ -17,7 +17,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 +35,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;

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", 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,6 +2,7 @@ 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";
@@ -10,8 +11,9 @@ 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 protectedSessionService from "./protected_session.js";
function registerEntrypoints() {
// hot keys are active also inside inputs and content editables
@@ -21,7 +23,7 @@ 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);
@@ -31,11 +33,14 @@ function registerEntrypoints() {
$("#recent-changes-button").click(recentChangesDialog.showDialog);
$("#protected-session-on").click(protectedSessionService.enterProtectedSession);
$("#protected-session-off").click(protectedSessionService.leaveProtectedSession);
$("#recent-notes-button").click(recentNotesDialog.showDialog);
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
$("#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);
@@ -45,11 +50,21 @@ function registerEntrypoints() {
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 +116,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

@@ -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>", {

View File

@@ -22,6 +22,7 @@ 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");
@@ -116,9 +117,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 +151,8 @@ async function loadNoteDetail(noteId) {
$noteIdDisplay.html(noteId);
setNoteBackgroundIfProtected(currentNote);
await handleProtectedSession();
$noteDetailWrapper.show();
@@ -170,7 +173,6 @@ async function loadNoteDetail(noteId) {
noteChangeDisabled = false;
}
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top

View File

@@ -64,24 +64,27 @@ 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")) {
const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
bundleService.executeBundle(bundle);
}
if (currentNote.mime.endsWith("env=backend")) {
await server.post('script/run/' + noteDetailService.getCurrentNoteId());
}
infoService.showMessage("Note executed");
}
$(document).bind('keydown', "ctrl+return", executeCurrentNote);

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,7 +11,13 @@ async function show() {
textEditor = await BalloonEditor.create($noteDetailText[0], {});
textEditor.model.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();
}
}
);
}
textEditor.setData(noteDetailService.getCurrentNote().content);

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

@@ -1,5 +1,6 @@
import treeService from './tree.js';
import server from './server.js';
import treeUtils from "./tree_utils.js";
const $tree = $("#tree");
const $searchInput = $("input[name='search-text']");
@@ -7,40 +8,62 @@ const $resetSearchButton = $("#reset-search-button");
const $doSearchButton = $("#do-search-button");
const $saveSearchButton = $("#save-search-button");
const $searchBox = $("#search-box");
const $searchResults = $("#search-results");
const $searchResultsInner = $("#search-results-inner");
const $closeSearchButton = $("#close-search-button");
function showSearch() {
$searchBox.show();
$searchInput.focus();
}
function hideSearch() {
resetSearch();
$searchResults.hide();
$searchBox.hide();
}
function toggleSearch() {
if ($searchBox.is(":hidden")) {
$searchBox.show();
$searchInput.focus();
showSearch();
}
else {
resetSearch();
$searchBox.hide();
hideSearch();
}
}
function resetSearch() {
$searchInput.val("");
getTree().clearFilter();
}
function getTree() {
return $tree.fancytree('getTree');
}
async function doSearch() {
const searchText = $searchInput.val();
const noteIds = await server.get('search/' + encodeURIComponent(searchText));
for (const noteId of noteIds) {
await treeService.expandToNote(noteId, {noAnimation: true, noEvents: true});
async function doSearch(searchText) {
if (searchText) {
$searchInput.val(searchText);
}
else {
searchText = $searchInput.val();
}
// Pass a string to perform case insensitive matching
getTree().filterBranches(node => noteIds.includes(node.data.noteId));
const results = await server.get('search/' + encodeURIComponent(searchText));
$searchResultsInner.empty();
$searchResults.show();
for (const result of results) {
const link = $('<a>', {
href: 'javascript:',
text: result.title
}).attr('action', 'note').attr('note-path', result.path);
const $result = $('<li>').append(link);
$searchResultsInner.append($result);
}
}
async function saveSearch() {
@@ -71,6 +94,11 @@ $resetSearchButton.click(resetSearch);
$saveSearchButton.click(saveSearch);
$closeSearchButton.click(hideSearch);
export default {
toggleSearch
toggleSearch,
resetSearch,
showSearch,
doSearch
};

View File

@@ -5,7 +5,7 @@ import infoService from "./info.js";
function getHeaders() {
let protectedSessionId = null;
try { // this is because protected session might not be declared in some cases - like when it's included in migration page
try { // this is because protected session might not be declared in some cases
protectedSessionId = protectedSessionHolder.getProtectedSessionId();
}
catch(e) {}

View File

@@ -17,11 +17,11 @@ import Branch from '../entities/branch.js';
import NoteShort from '../entities/note_short.js';
const $tree = $("#tree");
const $parentList = $("#parent-list");
const $parentListList = $("#parent-list-inner");
const $createTopLevelNoteButton = $("#create-top-level-note-button");
const $collapseTreeButton = $("#collapse-tree-button");
const $scrollToCurrentNoteButton = $("#scroll-to-current-note-button");
const $notePathList = $("#note-path-list");
const $notePathCount = $("#note-path-count");
let startNotePath = null;
@@ -81,11 +81,15 @@ async function expandToNote(notePath, expandOpts) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath);
let parentNoteId = 'root';
let parentNoteId = 'none';
for (const childNoteId of runPath) {
const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
if (!node) {
console.log(`Can't find node for noteId=${childNoteId} with parentNoteId=${parentNoteId}`);
}
if (childNoteId === noteId) {
return node;
}
@@ -115,7 +119,10 @@ async function getRunPath(notePath) {
utils.assertArguments(notePath);
const path = notePath.split("/").reverse();
path.push('root');
if (!path.includes("root")) {
path.push('root');
}
const effectivePath = [];
let childNoteId = null;
@@ -151,6 +158,8 @@ async function getRunPath(notePath) {
for (const noteId of pathToRoot) {
effectivePath.push(noteId);
}
effectivePath.push('root');
}
break;
@@ -162,7 +171,7 @@ async function getRunPath(notePath) {
}
}
if (parentNoteId === 'root') {
if (parentNoteId === 'none') {
break;
}
else {
@@ -174,40 +183,29 @@ async function getRunPath(notePath) {
return effectivePath.reverse();
}
async function showParentList(noteId, node) {
async function showPaths(noteId, node) {
utils.assertArguments(noteId, node);
const note = await treeCache.getNote(noteId);
const parents = await note.getParentNotes();
if (!parents.length) {
infoService.throwError("Can't find parents for noteId=" + noteId);
}
$notePathCount.html(parents.length + " path" + (parents.length > 0 ? "s" : ""));
if (parents.length <= 1) {
$parentList.hide();
}
else {
$parentList.show();
$parentListList.empty();
$notePathList.empty();
for (const parentNote of parents) {
const parentNotePath = await getSomeNotePath(parentNote);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
const title = await treeUtils.getNotePathTitle(notePath);
for (const parentNote of parents) {
const parentNotePath = await getSomeNotePath(parentNote);
// this is to avoid having root notes leading '/'
const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
const title = await treeUtils.getNotePathTitle(notePath);
let item;
const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
if (node.getParent().data.noteId === parentNote.noteId) {
item = $("<span/>").attr("title", "Current note").append(title);
}
else {
item = linkService.createNoteLink(notePath, title);
}
$parentListList.append($("<li/>").append(item));
if (node.getParent().data.noteId === parentNote.noteId) {
item.addClass("current");
}
$notePathList.append(item);
}
}
@@ -285,15 +283,16 @@ async function treeInitialized() {
}
}
function initFancyTree(branch) {
utils.assertArguments(branch);
function initFancyTree(tree) {
utils.assertArguments(tree);
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch,
source: tree,
scrollParent: $tree,
minExpandLevel: 2, // root can't be collapsed
click: (event, data) => {
const targetType = data.targetType;
const node = data.node;
@@ -319,7 +318,7 @@ function initFancyTree(branch) {
noteDetailService.switchToNote(node.noteId);
showParentList(node.noteId, data.node);
showPaths(node.noteId, data.node);
},
expand: (event, data) => setExpandedToServer(data.node.data.branchId, true),
collapse: (event, data) => setExpandedToServer(data.node.data.branchId, false),
@@ -375,7 +374,7 @@ async function loadTree() {
startNotePath = getNotePathFromAddress();
}
return await treeBuilder.prepareTree(resp.notes, resp.branches);
return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
}
function collapseTree(node = null) {

View File

@@ -5,12 +5,12 @@ import server from "./server.js";
import treeCache from "./tree_cache.js";
import messagingService from "./messaging.js";
async function prepareTree(noteRows, branchRows) {
utils.assertArguments(noteRows);
async function prepareTree(noteRows, branchRows, relations) {
utils.assertArguments(noteRows, branchRows, relations);
treeCache.load(noteRows, branchRows);
treeCache.load(noteRows, branchRows, relations);
return await prepareRealBranch(await treeCache.getNote('root'));
return [ await prepareNode(await treeCache.getBranch('root')) ];
}
async function prepareBranch(note) {
@@ -22,6 +22,35 @@ async function prepareBranch(note) {
}
}
async function prepareNode(branch) {
const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
isProtected: note.isProtected,
title: utils.escapeHtml(title),
extraClasses: await getExtraClasses(note),
refKey: note.noteId,
expanded: note.type !== 'search' && branch.isExpanded
};
if (note.hasChildren() || note.type === 'search') {
node.folder = true;
if (node.expanded && note.type !== 'search') {
node.children = await prepareRealBranch(note);
}
else {
node.lazy = true;
}
}
return node;
}
async function prepareRealBranch(parentNote) {
utils.assertArguments(parentNote);
@@ -35,32 +64,7 @@ async function prepareRealBranch(parentNote) {
const noteList = [];
for (const branch of childBranches) {
const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + " - ") : "") + note.title;
const node = {
noteId: note.noteId,
parentNoteId: branch.parentNoteId,
branchId: branch.branchId,
isProtected: note.isProtected,
title: utils.escapeHtml(title),
extraClasses: await getExtraClasses(note),
refKey: note.noteId,
expanded: note.type !== 'search' && branch.isExpanded
};
const hasChildren = (await note.getChildNotes()).length > 0;
if (hasChildren || note.type === 'search') {
node.folder = true;
if (node.expanded && note.type !== 'search') {
node.children = await prepareRealBranch(note);
}
else {
node.lazy = true;
}
}
const node = await prepareNode(branch);
noteList.push(node);
}
@@ -70,14 +74,21 @@ async function prepareRealBranch(parentNote) {
async function prepareSearchBranch(note) {
const fullNote = await noteDetailService.loadNote(note.noteId);
const noteIds = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
const results = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
const noteIds = results.map(res => res.noteId);
// force to load all the notes at once instead of one by one
await treeCache.getNotes(noteIds);
for (const result of results) {
const origBranch = await treeCache.getBranch(result.branchId);
for (const noteId of noteIds) {
const branch = new Branch(treeCache, {
branchId: "virt" + utils.randomString(10),
noteId: noteId,
noteId: result.noteId,
parentNoteId: note.noteId,
prefix: '',
prefix: origBranch.prefix,
virtual: true
});
@@ -92,11 +103,15 @@ async function getExtraClasses(note) {
const extraClasses = [];
if (note.noteId === 'root') {
extraClasses.push("tree-root");
}
if (note.isProtected) {
extraClasses.push("protected");
}
if ((await note.getParentNotes()).length > 1) {
if (note.getParentNoteIds().length > 1) {
extraClasses.push("multiple-parents");
}

View File

@@ -2,45 +2,93 @@ import utils from "./utils.js";
import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js";
import infoService from "./info.js";
import server from "./server.js";
class TreeCache {
load(noteRows, branchRows) {
this.parents = [];
this.children = [];
load(noteRows, branchRows, relations) {
this.parents = {};
this.children = {};
this.childParentToBranch = {};
/** @type {Object.<string, NoteShort>} */
this.notes = {};
/** @type {Object.<string, Branch>} */
this.branches = {};
this.addResp(noteRows, branchRows, relations);
}
addResp(noteRows, branchRows, relations) {
for (const noteRow of noteRows) {
const note = new NoteShort(this, noteRow);
this.notes[note.noteId] = note;
}
/** @type {Object.<string, Branch>} */
this.branches = {};
for (const branchRow of branchRows) {
const branch = new Branch(this, branchRow);
this.addBranch(branch);
}
for (const relation of relations) {
this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId);
}
}
async getNotes(noteIds) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
if (missingNoteIds.length > 0) {
const resp = await server.post('tree/load', { noteIds: missingNoteIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return noteIds.map(noteId => {
if (!this.notes[noteId]) {
throw new Error(`Can't find note ${noteId}`);
}
else {
return this.notes[noteId];
}
});
}
/** @return NoteShort */
async getNote(noteId) {
return this.notes[noteId];
if (noteId === 'none') {
return null;
}
return (await this.getNotes([noteId]))[0];
}
addBranch(branch) {
this.branches[branch.branchId] = branch;
this.parents[branch.noteId] = this.parents[branch.noteId] || [];
this.parents[branch.noteId].push(this.notes[branch.parentNoteId]);
this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
}
this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || [];
this.children[branch.parentNoteId].push(this.notes[branch.noteId]);
addBranchRelationship(branchId, childNoteId, parentNoteId) {
if (parentNoteId === 'none') { // applies only to root element
return;
}
this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch;
this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
this.parents[childNoteId] = this.parents[childNoteId] || [];
if (!this.parents[childNoteId].includes(parentNoteId)) {
this.parents[childNoteId].push(parentNoteId);
}
this.children[parentNoteId] = this.children[parentNoteId] || [];
if (!this.children[parentNoteId].includes(childNoteId)) {
this.children[parentNoteId].push(childNoteId);
}
}
add(note, branch) {
@@ -49,21 +97,46 @@ class TreeCache {
this.addBranch(branch);
}
async getBranches(branchIds) {
const missingBranchIds = branchIds.filter(branchId => this.branches[branchId] === undefined);
if (missingBranchIds.length > 0) {
const resp = await server.post('tree/load', { branchIds: branchIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return branchIds.map(branchId => {
if (!this.branches[branchId]) {
throw new Error(`Can't find branch ${branchId}`);
}
else {
return this.branches[branchId];
}
});
}
/** @return Branch */
async getBranch(branchId) {
return this.branches[branchId];
return (await this.getBranches([branchId]))[0];
}
/** @return Branch */
async getBranchByChildParent(childNoteId, parentNoteId) {
const key = (childNoteId + '-' + parentNoteId);
const branch = this.childParentToBranch[key];
const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
if (!branch) {
return await this.getBranch(branchId);
}
getBranchIdByChildParent(childNoteId, parentNoteId) {
const key = childNoteId + '-' + parentNoteId;
const branchId = this.childParentToBranch[key];
if (!branchId) {
infoService.throwError("Cannot find branch for child-parent=" + key);
}
return branch;
return branchId;
}
/* Move note from one parent to another. */
@@ -78,33 +151,14 @@ class TreeCache {
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
// add new associations
treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId));
treeCache.parents[childNoteId].push(newParentNoteId);
treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child
treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId));
}
removeParentChildRelation(parentNoteId, childNoteId) {
utils.assertArguments(parentNoteId, childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
}
async setParentChildRelation(branchId, parentNoteId, childNoteId) {
treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId));
treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId));
treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId);
treeCache.children[newParentNoteId].push(childNoteId);
}
}

View File

@@ -52,6 +52,15 @@ async function getNotePathTitle(notePath) {
const titlePath = [];
if (notePath.startsWith('root/')) {
notePath = notePath.substr(5);
}
// special case when we want just root's title
if (notePath === 'root') {
return await getNoteTitle(notePath);
}
let parentNoteId = 'root';
for (const noteId of notePath.split('/')) {

View File

@@ -0,0 +1,51 @@
import server from "./server.js";
import utils from "./utils.js";
import optionsInitService from "./options_init.js";
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 2.0;
async function decreaseZoomFactor() {
await setZoomFactorAndSave(getCurrentZoom() - 0.1);
}
async function increaseZoomFactor() {
await setZoomFactorAndSave(getCurrentZoom() + 0.1);
}
function setZoomFactor(zoomFactor) {
zoomFactor = parseFloat(zoomFactor);
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(zoomFactor);
}
async function setZoomFactorAndSave(zoomFactor) {
if (!utils.isElectron()) {
return;
}
if (zoomFactor >= MIN_ZOOM && zoomFactor <= MAX_ZOOM) {
setZoomFactor(zoomFactor);
await server.put('options/zoomFactor/' + zoomFactor);
}
else {
console.log(`Zoom factor ${zoomFactor} outside of the range, ignored.`);
}
}
function getCurrentZoom() {
return require('electron').webFrame.getZoomFactor();
}
if (utils.isElectron()) {
optionsInitService.optionsReady.then(options => setZoomFactor(options.zoomFactor))
}
export default {
decreaseZoomFactor,
increaseZoomFactor,
setZoomFactor,
setZoomFactorAndSave
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
grid-template-areas: "header header"
"left-pane title"
"left-pane note-detail";
grid-template-columns: 2fr 5fr;
grid-template-columns: 29% 70%;
grid-template-rows: auto
auto
1fr;
@@ -20,6 +20,7 @@
background-color: #f1f1f1;
display: flex;
align-items: center;
padding: 4px;
}
#note-detail-wrapper {
@@ -33,7 +34,7 @@
}
#note-detail-component-wrapper {
flex-grow: 1;
flex-grow: 100;
position: relative;
overflow: auto;
flex-basis: content;
@@ -51,7 +52,7 @@
overflow: auto;
}
#note-detail-wrapper.protected, #note-detail-wrapper.protected .CodeMirror {
#note-detail-component-wrapper.protected, #note-detail-component-wrapper.protected .CodeMirror {
background-color: #eee;
}
@@ -61,38 +62,36 @@
}
ul.fancytree-container {
overflow: auto;
position: relative;
outline: none !important;
}
/* icons from https://feathericons.com */
span.fancytree-node > span.fancytree-icon {
background: url("../images/icons/file.png") 0 0;
background: url("../images/icons/file-16.png") 0 0;
}
span.fancytree-node.fancytree-folder > span.fancytree-icon {
background: url("../images/icons/folder.png") 0 0;
background: url("../images/icons/folder-16.png") 0 0;
}
span.fancytree-node.code > span.fancytree-icon {
background: url("../images/icons/code.png") 0 0;
background: url("../images/icons/code-16.png") 0 0;
}
span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
background: url("../images/icons/code-folder.png") 0 0;
background: url("../images/icons/code-folder-16.png") 0 0;
}
span.fancytree-node.file > span.fancytree-icon {
background: url("../images/icons/paperclip.png") 0 0;
background: url("../images/icons/paperclip-16.png") 0 0;
}
span.fancytree-node.render > span.fancytree-icon {
background: url("../images/icons/play.png") 0 0;
background: url("../images/icons/play-16.png") 0 0;
}
span.fancytree-node.search > span.fancytree-icon {
background: url("../images/icons/search-small.png") 0 0;
background: url("../images/icons/search-small-16.png") 0 0;
}
span.fancytree-node.protected > span.fancytree-icon {
@@ -107,6 +106,15 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
font-weight: bold;
}
span.fancytree-node.tree-root > span.fancytree-icon {
background: url("../images/icons/tree-root-16.png") 0 0;
}
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
.ui-fancytree > li > ul {
padding-left: 5px;
}
/* By default not focused active tree item is not easily visible, this makes it more visible */
span.fancytree-active:not(.fancytree-focused) .fancytree-title {
background-color: #ddd !important;
@@ -131,10 +139,6 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
width: 24px;
}
#protect-button, #unprotect-button {
display: none;
}
.ui-widget-content a:not(.ui-tabs-anchor) {
color: #337ab7 !important;
}
@@ -161,16 +165,27 @@ div.ui-tooltip {
width: auto;
}
#parent-list {
display: none;
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
grid-area: parent-list;
#tree {
overflow: auto;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px;
}
#parent-list ul {
padding-left: 20px;
#search-results {
padding: 0 5px 5px 15px;
flex-basis: 40%;
flex-grow: 1;
flex-shrink: 1;
margin-top: 10px;
display: none;
overflow: auto;
border-bottom: 2px solid #ddd;
}
#search-results ul {
padding: 5px 5px 5px 15px;
}
/*
@@ -230,16 +245,16 @@ div.ui-tooltip {
}
.suppressed {
filter: opacity(7%);
display: none;
}
.dropdown-menu li:not(.divider) {
#note-type .dropdown-menu li:not(.divider) {
padding: 5px;
width: 200px;
}
.dropdown-menu li:not(.divider):hover, .dropdown-menu li:not(.divider) a:hover {
background-color: #eee !important;
background-color: #ccc !important;
cursor: pointer;
}
@@ -261,11 +276,20 @@ div.ui-tooltip {
#note-detail-code {
min-height: 200px;
overflow: auto;
}
#note-detail-render {
min-height: 200px;
}
.CodeMirror {
height: 100%;
font-family: "Liberation Mono", "Lucida Console", monospace;
height: auto;
}
.CodeMirror-scroll {
min-height: 200px;
}
#note-id-display {
@@ -330,4 +354,70 @@ div.ui-tooltip {
.child-overview a {
color: #444;
}
#sql-console-query {
height: 150px;
width: 100%;
border: 1px solid #ccc;
margin-bottom: 10px;
}
#sql-console-query .CodeMirror {
height: 150px;
}
#history-navigation {
margin: 0 20px 0 5px;
display: flex;
}
.btn:not(.btn-primary):not(.btn-danger) {
border-color: #bbb;
background-color: #eee;
}
.btn.active:not(.btn-primary) {
background-color: #ccc;
}
#note-path-list .current a {
font-weight: bold;
}
button.icon-button {
height: 28px;
width: 28px;
background: no-repeat center;
}
#note-actions {
margin-left: 10px;
margin-right: 10px;
}
#note-actions .dropdown-menu {
width: 15em;
}
/* Themes */
html.theme-black, html.theme-black img, html.theme-black video {
filter: invert(100%) hue-rotate(180deg);
}
html.theme-black body {
background: black;
}
html.theme-dark {
filter: invert(90%) hue-rotate(180deg);
}
html.theme-dark img, html.theme-dark video {
filter: invert(100%) hue-rotate(180deg);
}
html.theme-dark body {
background: #191819;
}

View File

@@ -0,0 +1,20 @@
"use strict";
const noteCacheService = require('../../services/note_cache');
async function getAutocomplete(req) {
const query = req.query.query;
const results = noteCacheService.findNotes(query);
return results.map(res => {
return {
value: res.title + ' (' + res.path + ')',
label: res.title
}
});
}
module.exports = {
getAutocomplete
};

View File

@@ -3,17 +3,7 @@
const sql = require('../../services/sql');
async function getEventLog() {
await deleteOld();
return await sql.getRows("SELECT * FROM event_log ORDER BY dateAdded DESC");
}
async function deleteOld() {
const cutoffId = await sql.getValue("SELECT id FROM event_log ORDER BY id DESC LIMIT 1000, 1");
if (cutoffId) {
await sql.execute("DELETE FROM event_log WHERE id < ?", [cutoffId]);
}
return await sql.getRows("SELECT * FROM event_log ORDER BY dateCreated DESC");
}
module.exports = {

View File

@@ -5,12 +5,85 @@ const html = require('html');
const tar = require('tar-stream');
const sanitize = require("sanitize-filename");
const repository = require("../../services/repository");
const utils = require('../../services/utils');
async function exportNote(req, res) {
const noteId = req.params.noteId;
const format = req.params.format;
const branchId = await sql.getValue('SELECT branchId FROM branches WHERE noteId = ?', [noteId]);
if (format === 'tar') {
await exportToTar(branchId, res);
}
else if (format === 'opml') {
await exportToOpml(branchId, res);
}
else {
return [404, "Unrecognized export format " + format];
}
}
function escapeXmlAttribute(text) {
return text.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function prepareText(text) {
const newLines = text.replace(/(<p[^>]*>|<br\s*\/?>)/g, '\n')
.replace(/&nbsp;/g, ' '); // nbsp isn't in XML standard (only HTML)
const stripped = utils.stripTags(newLines);
const escaped = escapeXmlAttribute(stripped);
return escaped.replace(/\n/g, '&#10;');
}
async function exportToOpml(branchId, res) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
const sanitizedTitle = sanitize(title);
async function exportNoteInner(branchId) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
const preparedTitle = prepareText(title);
const preparedContent = prepareText(note.content);
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
for (const child of await note.getChildBranches()) {
await exportNoteInner(child.branchId);
}
res.write('</outline>');
}
res.setHeader('Content-Disposition', 'file; filename="' + sanitizedTitle + '.opml"');
res.setHeader('Content-Type', 'text/x-opml');
res.write(`<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>Trilium export</title>
</head>
<body>`);
await exportNoteInner(branchId);
res.write(`</body>
</opml>`);
res.end();
}
async function exportToTar(branchId, res) {
const pack = tar.pack();
const exportedNoteIds = [];

View File

@@ -7,6 +7,79 @@ const Branch = require('../../entities/branch');
const tar = require('tar-stream');
const stream = require('stream');
const path = require('path');
const parseString = require('xml2js').parseString;
async function importToBranch(req) {
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const parentNote = await repository.getNote(parentNoteId);
if (!parentNote) {
return [404, `Note ${parentNoteId} doesn't exist.`];
}
const extension = path.extname(file.originalname).toLowerCase();
if (extension === '.tar') {
await importTar(file, parentNoteId);
}
else if (extension === '.opml') {
return await importOpml(file, parentNoteId);
}
else {
return [400, `Unrecognized extension ${extension}, must be .tar or .opml`];
}
}
function toHtml(text) {
if (!text) {
return '';
}
return '<p>' + text.replace(/(?:\r\n|\r|\n)/g, '</p><p>') + '</p>';
}
async function importOutline(outline, parentNoteId) {
const {note} = await noteService.createNote(parentNoteId, outline.$.title, toHtml(outline.$.text));
for (const childOutline of (outline.outline || [])) {
await importOutline(childOutline, note.noteId);
}
}
async function importOpml(file, parentNoteId) {
const xml = await new Promise(function(resolve, reject)
{
parseString(file.buffer, function (err, result) {
if (err) {
reject(err);
}
else {
resolve(result);
}
});
});
if (xml.opml.$.version !== '1.0' && xml.opml.$.version !== '1.1') {
return [400, 'Unsupported OPML version ' + xml.opml.$.version + ', 1.0 or 1.1 expected instead.'];
}
const outlines = xml.opml.body[0].outline || [];
for (const outline of outlines) {
await importOutline(outline, parentNoteId);
}
}
async function importTar(file, parentNoteId) {
const files = await parseImportFile(file);
// maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {};
await importNotes(files, parentNoteId, noteIdMap);
}
function getFileName(name) {
let key;
@@ -86,24 +159,6 @@ async function parseImportFile(file) {
});
}
async function importTar(req) {
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const parentNote = await repository.getNote(parentNoteId);
if (!parentNote) {
return [404, `Note ${parentNoteId} doesn't exist.`];
}
const files = await parseImportFile(file);
// maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {};
await importNotes(files, parentNoteId, noteIdMap);
}
async function importNotes(files, parentNoteId, noteIdMap) {
for (const file of files) {
if (file.meta.version !== 1) {
@@ -143,5 +198,5 @@ async function importNotes(files, parentNoteId, noteIdMap) {
}
module.exports = {
importTar
importToBranch
};

View File

@@ -7,6 +7,8 @@ const sourceIdService = require('../../services/source_id');
const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
async function loginSync(req) {
const timestampStr = req.body.timestamp;
@@ -19,10 +21,10 @@ async function loginSync(req) {
return [400, { message: 'Auth request time is out of sync' }];
}
const dbVersion = req.body.dbVersion;
const syncVersion = req.body.syncVersion;
if (dbVersion !== appInfo.dbVersion) {
return [400, { message: 'Non-matching db versions, local is version ' + appInfo.dbVersion }];
if (syncVersion !== appInfo.syncVersion) {
return [400, { message: 'Non-matching sync versions, local is version ' + appInfo.syncVersion }];
}
const documentSecret = await options.getOption('documentSecret');
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
const decryptedDataKey = await passwordEncryptionService.getDataKey(password);
const protectedSessionId = protectedSessionService.setDataKey(req, decryptedDataKey);
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.namespace.set('protectedSessionId', protectedSessionId);
eventService.emit(eventService.ENTER_PROTECTED_SESSION);
return {
success: true,

View File

@@ -1,25 +0,0 @@
"use strict";
const optionService = require('../../services/options');
const migrationService = require('../../services/migration');
const appInfo = require('../../services/app_info');
async function getMigrationInfo() {
return {
dbVersion: parseInt(await optionService.getOption('dbVersion')),
appDbVersion: appInfo.dbVersion
};
}
async function executeMigration() {
const migrations = await migrationService.migrate();
return {
migrations: migrations
};
}
module.exports = {
getMigrationInfo,
executeMigration
};

View File

@@ -47,7 +47,7 @@ async function sortNotes(req) {
async function protectBranch(req) {
const noteId = req.params.noteId;
const note = repository.getNote(noteId);
const note = await repository.getNote(noteId);
const protect = !!parseInt(req.params.isProtected);
await noteService.protectNoteRecursively(note, protect);

View File

@@ -2,9 +2,10 @@
const sql = require('../../services/sql');
const optionService = require('../../services/options');
const log = require('../../services/log');
// options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval'];
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', 'zoomFactor', 'theme'];
async function getOptions() {
const options = await sql.getMap("SELECT name, value FROM options WHERE name IN ("
@@ -20,6 +21,8 @@ async function updateOption(req) {
return [400, "not allowed option to set"];
}
log.info(`Updating option ${name} to ${value}`);
await optionService.setOption(name, value);
}

View File

@@ -1,12 +1,12 @@
"use strict";
const repository = require('../../services/repository');
const dateUtils = require('../../services/date_utils');
const optionService = require('../../services/options');
const RecentNote = require('../../entities/recent_note');
const noteCacheService = require('../../services/note_cache');
async function getRecentNotes() {
return await repository.getEntities(`
const recentNotes = await repository.getEntities(`
SELECT
recent_notes.*
FROM
@@ -16,8 +16,14 @@ async function getRecentNotes() {
recent_notes.isDeleted = 0
AND branches.isDeleted = 0
ORDER BY
dateAccessed DESC
dateCreated DESC
LIMIT 200`);
for (const rn of recentNotes) {
rn.title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
}
return recentNotes;
}
async function addRecentNote(req) {
@@ -26,9 +32,7 @@ async function addRecentNote(req) {
await new RecentNote({
branchId: branchId,
notePath: notePath,
dateAccessed: dateUtils.nowDate(),
isDeleted: 0
notePath: notePath
}).save();
await optionService.setOption('startNotePath', notePath);

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