Compare commits

..

46 Commits

Author SHA1 Message Date
azivner
3972c27e7a release 0.17.0 2018-07-09 21:22:12 +02:00
azivner
14cffbbe62 docker commands with sudo 2018-07-09 21:21:28 +02:00
azivner
599c3c04af correct sourceMappingURL which gets rid of error message, fixes #114 2018-07-08 23:23:49 +02:00
azivner
f1412b631d hide log polluting error message 2018-07-08 23:13:56 +02:00
azivner
41908050bb ctrl+u now doesn't trigger show source since it's occupied by underline 2018-07-08 21:45:05 +02:00
azivner
f07033423c ckeditor upgraded to 10.1.0 plus new plugins (table, strikethrough, underline) 2018-07-08 21:21:52 +02:00
azivner
daf96fcbf2 using zeit/pkg to package easy to use linux server edition 2018-07-06 00:05:06 +02:00
azivner
2bca94529e minor upgrades 2018-07-05 23:17:53 +02:00
azivner
b2c9a0da21 fix note sources, fixes #123 2018-07-04 20:37:23 +02:00
azivner
7a9542b4fc release 0.16.0 2018-06-16 13:34:39 -04:00
azivner
3a95c9e1bc all dialogs are now non-modal because of high cpu usage 2018-06-16 13:31:56 -04:00
azivner
3d2ef6be01 remove optionId, closes #117 2018-06-13 19:10:28 -04:00
azivner
d67246699a Introduced separate sync version (previously DB version was used to check sync compatibility), closes #120 2018-06-10 15:55:29 -04:00
azivner
14c704d6db db upgrades are now handled transparently in the background without bothering the user, closes #119 2018-06-10 15:49:22 -04:00
azivner
4c8eeb2e6f added docker build, closes #106 2018-06-10 15:06:52 -04:00
azivner
c1b245c8b1 fix unnecessary change events, closes #118 2018-06-10 11:51:13 -04:00
azivner
74202d67bb got rid of "Trilium Notes" branding - not necessary and takes valuable space 2018-06-10 10:57:45 -04:00
azivner
26066f39b1 chaged "focused mode" - now title is displayed as well and together with content takes whole window 2018-06-10 10:53:39 -04:00
azivner
b255cf190c fixes for zoom factor setting 2018-06-09 10:34:51 -04:00
azivner
bc77b143b0 darker outlines so inverted dark themes are more visible 2018-06-09 09:48:18 -04:00
azivner
9f0ff6ae7a note actions dropdown sizing 2018-06-09 09:44:40 -04:00
azivner
736704c7d6 fix show paths 2018-06-09 09:32:13 -04:00
azivner
654c116c58 use backgrounds for icon buttons so that dark and black themes look better 2018-06-09 09:28:50 -04:00
azivner
89a5cab98f added too options new tab appearance with possibility to change theme (white, black, dark) and zoom factor 2018-06-08 23:18:53 -04:00
azivner
c39d0be8cd refactoring of icon button styles 2018-06-08 22:17:00 -04:00
azivner
e75b4cd848 execute on script note is icon, closes #116 2018-06-08 21:59:40 -04:00
azivner
378e8f35e5 release 0.15.0 2018-06-07 23:09:21 -04:00
azivner
bdb5e2f13f no results also for "add link" 2018-06-07 23:08:41 -04:00
azivner
8211bed449 renamed icons according to their size, fixes #113 2018-06-07 23:02:21 -04:00
azivner
b243632483 usability improvements to autocomplete ("no results" etc.), needs refactoring 2018-06-07 20:18:46 -04:00
azivner
e4d2513451 close search button 2018-06-07 19:50:16 -04:00
azivner
385144451b renamed hideInAutocomplete label to archived 2018-06-07 19:26:28 -04:00
azivner
c8c533844e icons for render & toggle edit since text titles caused unwanted horizontal scrolling in smaller window sizes 2018-06-06 23:37:57 -04:00
azivner
0e69f0c079 fix recent notes issues 2018-06-06 22:38:36 -04:00
azivner
aee60c444f added "show results in full text" 2018-06-05 23:28:10 -04:00
azivner
e7a504c66b fixes and optimizations for search 2018-06-05 22:47:47 -04:00
azivner
45d9c7164c search refactorings 2018-06-05 19:12:52 -04:00
azivner
bd913a63a8 search note fixes 2018-06-04 23:21:45 -04:00
azivner
5a1938c078 better sizing of search pane 2018-06-04 20:22:41 -04:00
azivner
015cd68756 renaming/refactoring of search services 2018-06-04 19:48:02 -04:00
azivner
76c0e5b2b8 new UI for search, closes #108 (still needs cleanup) 2018-06-03 20:42:25 -04:00
azivner
0f8f707acd persisting zoom setting in electron, fixes #112 2018-06-02 13:02:20 -04:00
azivner
083cccea28 better protected/unprotected note indicator, fixes #110 2018-06-02 11:47:16 -04:00
azivner
31b76b23ce release 0.14.1 2018-06-02 09:39:37 -04:00
azivner
af529f82e5 fixed false sync error reporting 2018-06-02 09:39:04 -04:00
azivner
fc6669d254 initialization and schema fixes, closes #111 2018-06-01 22:26:37 -04:00
86 changed files with 4152 additions and 3430 deletions

4
.dockerignore Normal file
View File

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

View File

@@ -142,11 +142,10 @@ parentNoteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
</key>
<column id="42" 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="43" parent="8" name="noteId">
<Position>2</Position>
@@ -161,505 +160,517 @@ parentNoteId</ColNames>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<key id="46" 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="47" parent="9" name="imageId">
<column id="48" parent="9" name="imageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="48" parent="9" name="format">
<column id="49" parent="9" name="format">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="49" parent="9" name="checksum">
<column id="50" parent="9" name="checksum">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="50" parent="9" name="name">
<column id="51" parent="9" name="name">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="51" parent="9" name="data">
<column id="52" parent="9" name="data">
<Position>5</Position>
<DataType>BLOB|0s</DataType>
</column>
<column id="52" 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="53" parent="9" name="dateModified">
<column id="54" parent="9" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="54" parent="9" name="dateCreated">
<column id="55" parent="9" name="dateCreated">
<Position>8</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="55" parent="9" name="hash">
<column id="56" parent="9" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="56" parent="9" name="sqlite_autoindex_images_1">
<index id="57" parent="9" name="sqlite_autoindex_images_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>imageId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="57" parent="9">
<key id="58" parent="9">
<ColNames>imageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
</key>
<column id="58" parent="10" name="labelId">
<column id="59" parent="10" name="labelId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="59" parent="10" name="noteId">
<column id="60" parent="10" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="60" parent="10" name="name">
<column id="61" parent="10" name="name">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="61" 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="62" 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="63" parent="10" name="dateCreated">
<column id="64" parent="10" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="64" parent="10" name="dateModified">
<column id="65" parent="10" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="65" parent="10" name="isDeleted">
<column id="66" parent="10" name="isDeleted">
<Position>8</Position>
<DataType>INT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="66" parent="10" name="hash">
<column id="67" parent="10" name="hash">
<Position>9</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="67" parent="10" name="sqlite_autoindex_labels_1">
<index id="68" parent="10" name="sqlite_autoindex_labels_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>labelId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="68" parent="10" name="IDX_labels_noteId">
<index id="69" parent="10" name="IDX_labels_noteId">
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<index id="69" 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="70" parent="10">
<key id="71" parent="10">
<ColNames>labelId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
</key>
<column id="71" parent="11" name="noteImageId">
<column id="72" parent="11" name="noteImageId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="72" parent="11" name="noteId">
<column id="73" parent="11" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="73" parent="11" name="imageId">
<column id="74" parent="11" name="imageId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="74" 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="75" parent="11" name="dateModified">
<column id="76" parent="11" name="dateModified">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="76" parent="11" name="dateCreated">
<column id="77" parent="11" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="77" parent="11" name="hash">
<column id="78" parent="11" name="hash">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="78" parent="11" name="sqlite_autoindex_note_images_1">
<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="79" 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="80" 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="81" 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="82" parent="11">
<key id="83" parent="11">
<ColNames>noteImageId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
</key>
<column id="83" parent="12" name="noteRevisionId">
<column id="84" parent="12" name="noteRevisionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="84" parent="12" name="noteId">
<column id="85" parent="12" name="noteId">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="85" parent="12" name="title">
<column id="86" parent="12" name="title">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="86" parent="12" name="content">
<column id="87" parent="12" name="content">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
</column>
<column id="87" 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="88" parent="12" name="dateModifiedFrom">
<column id="89" parent="12" name="dateModifiedFrom">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="89" parent="12" name="dateModifiedTo">
<column id="90" parent="12" name="dateModifiedTo">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="90" 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="91" 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>
<column id="92" parent="12" name="hash">
<column id="93" parent="12" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="93" parent="12" name="sqlite_autoindex_note_revisions_1">
<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="94" 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="95" 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="96" 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="97" parent="12">
<key id="98" parent="12">
<ColNames>noteRevisionId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
</key>
<column id="98" parent="13" name="noteId">
<column id="99" parent="13" name="noteId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="99" 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="100" 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="101" 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="102" 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="103" parent="13" name="dateCreated">
<column id="104" parent="13" name="dateCreated">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="104" parent="13" name="dateModified">
<column id="105" parent="13" name="dateModified">
<Position>7</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="105" 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="106" 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>
<column id="107" parent="13" name="hash">
<column id="108" parent="13" name="hash">
<Position>10</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="108" parent="13" name="sqlite_autoindex_notes_1">
<index id="109" parent="13" name="sqlite_autoindex_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>noteId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<index id="109" parent="13" name="IDX_notes_type">
<index id="110" parent="13" name="IDX_notes_type">
<ColNames>type</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="110" parent="13">
<key id="111" parent="13">
<ColNames>noteId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
</key>
<column id="111" parent="14" name="name">
<column id="112" parent="14" name="optionId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="112" parent="14" name="value">
<column id="113" parent="14" name="name">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="113" 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="114" 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>
<column id="115" parent="14" name="hash">
<Position>5</Position>
<column id="117" parent="14" name="hash">
<Position>6</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="116" parent="14" name="dateCreated">
<Position>6</Position>
<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="117" parent="14" name="sqlite_autoindex_options_1">
<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="118" 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="119" parent="15" name="branchId">
<column id="121" parent="15" name="branchId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="120" parent="15" name="notePath">
<column id="122" parent="15" name="notePath">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="121" parent="15" name="dateCreated">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="122" parent="15" name="isDeleted">
<Position>4</Position>
<DataType>INT|0s</DataType>
</column>
<column id="123" parent="15" name="hash">
<Position>5</Position>
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<index id="124" parent="15" name="sqlite_autoindex_recent_notes_1">
<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="126" parent="15" name="sqlite_autoindex_recent_notes_1">
<NameSurrogate>1</NameSurrogate>
<ColNames>branchId</ColNames>
<ColumnCollations></ColumnCollations>
<Unique>1</Unique>
</index>
<key id="125" parent="15">
<key id="127" parent="15">
<ColNames>branchId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
</key>
<column id="126" parent="16" name="sourceId">
<column id="128" parent="16" name="sourceId">
<Position>1</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="127" parent="16" name="dateCreated">
<column id="129" parent="16" name="dateCreated">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="128" 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="129" parent="16">
<key id="131" parent="16">
<ColNames>sourceId</ColNames>
<Primary>1</Primary>
<UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
</key>
<column id="130" parent="17" name="type">
<column id="132" parent="17" name="type">
<Position>1</Position>
<DataType>text|0s</DataType>
</column>
<column id="131" parent="17" name="name">
<column id="133" parent="17" name="name">
<Position>2</Position>
<DataType>text|0s</DataType>
</column>
<column id="132" parent="17" name="tbl_name">
<column id="134" parent="17" name="tbl_name">
<Position>3</Position>
<DataType>text|0s</DataType>
</column>
<column id="133" parent="17" name="rootpage">
<column id="135" parent="17" name="rootpage">
<Position>4</Position>
<DataType>integer|0s</DataType>
</column>
<column id="134" parent="17" name="sql">
<column id="136" parent="17" name="sql">
<Position>5</Position>
<DataType>text|0s</DataType>
</column>
<column id="135" parent="18" name="name">
<column id="137" parent="18" name="name">
<Position>1</Position>
</column>
<column id="136" parent="18" name="seq">
<column id="138" parent="18" name="seq">
<Position>2</Position>
</column>
<column id="137" 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="138" parent="19" name="entityName">
<column id="140" parent="19" name="entityName">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="139" parent="19" name="entityId">
<column id="141" parent="19" name="entityId">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="140" parent="19" name="sourceId">
<column id="142" parent="19" name="sourceId">
<Position>4</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<column id="141" parent="19" name="syncDate">
<column id="143" parent="19" name="syncDate">
<Position>5</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
</column>
<index id="142" 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="143" parent="19" name="IDX_sync_syncDate">
<index id="145" parent="19" name="IDX_sync_syncDate">
<ColNames>syncDate</ColNames>
<ColumnCollations></ColumnCollations>
</index>
<key id="144" 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
sudo 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
sudo docker push zadam/trilium:latest
sudo docker push zadam/trilium:$1

View File

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

View File

@@ -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
);

5992
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.14.0",
"version": "0.17.0",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
"trilium": "./src/www"
},
"repository": {
"type": "git",
"url": "https://github.com/zadam/trilium.git"
@@ -17,7 +20,8 @@
"start-forge": "electron-forge start",
"package-forge": "electron-forge package",
"make-forge": "electron-forge make",
"publish-forge": "electron-forge publish"
"publish-forge": "electron-forge publish",
"build-pkg": "pkg . --targets node8-linux-x64 --output dist/trilium-linux-x64-server.elf"
},
"dependencies": {
"async-mutex": "^0.1.3",
@@ -28,7 +32,7 @@
"debug": "~3.1.0",
"devtron": "^1.4.0",
"ejs": "~2.6.1",
"electron-debug": "^1.5.0",
"electron-debug": "^2.0.0",
"electron-dl": "^1.12.0",
"electron-in-page-search": "^1.3.2",
"express": "~4.16.3",
@@ -43,8 +47,8 @@
"imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5",
"jimp": "^0.2.28",
"moment": "^2.22.1",
"multer": "^1.3.0",
"moment": "^2.22.2",
"multer": "^1.3.1",
"open": "0.0.5",
"rand-token": "^0.4.0",
"rcedit": "^1.1.0",
@@ -59,18 +63,19 @@
"sqlite": "^2.9.2",
"tar-stream": "^1.6.1",
"unescape": "^1.0.1",
"ws": "^5.2.0",
"ws": "^5.2.1",
"xml2js": "^0.4.19"
},
"devDependencies": {
"electron": "^2.0.1",
"electron-compile": "^6.4.2",
"electron": "^2.0.4",
"electron-compile": "^6.4.3",
"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.21.1"
"electron-prebuilt-compile": "2.0.4",
"electron-rebuild": "^1.8.1",
"lorem-ipsum": "^1.0.5",
"tape": "^4.9.1",
"xo": "^0.21.1",
"pkg": "^4.3.3"
},
"config": {
"forge": {
@@ -109,5 +114,12 @@
"node",
"browser"
]
},
"pkg": {
"assets": [
"./db/**/*",
"./src/public/**/*",
"./src/views/**/*"
]
}
}

View File

@@ -21,10 +21,7 @@ class Entity {
contentToHash += "|" + this[propertyName];
}
// this IF is to ease the migration from before hashed options, can be later removed
if (this.constructor.tableName !== 'options' || this.isSynced) {
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
async save() {

View File

Before

Width:  |  Height:  |  Size: 511 B

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

View File

Before

Width:  |  Height:  |  Size: 511 B

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

View File

Before

Width:  |  Height:  |  Size: 240 B

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

@@ -57,7 +57,15 @@ async function showDialog() {
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
response(result);
if (result.length > 0) {
response(result);
}
else {
response([{
label: "No results",
value: "No results"
}]);
}
},
minLength: 2,
change: async () => {

View File

@@ -1,10 +1,13 @@
import treeService from '../services/tree.js';
import linkService from '../services/link.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;
@@ -20,7 +23,18 @@ async function showDialog() {
source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
response(result);
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
});
@@ -41,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

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

View File

@@ -1,9 +1,10 @@
"use strict";
import protectedSessionHolder from '../services/protected_session_holder.js';
import utils from '../services/utils.js';
import server from '../services/server.js';
import infoService from "../services/info.js";
import zoomService from "../services/zoom.js";
import utils from "../services/utils.js";
const $dialog = $("#options-dialog");
const $tabs = $("#options-tabs");
@@ -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

@@ -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() {
@@ -59,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

@@ -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';
@@ -47,7 +47,12 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
let message = "Uncaught error: ";
if (string.indexOf("script error") > -1){
if (string.includes("Cannot read property 'defaultView' of undefined")) {
// ignore this specific error which is very common but we don't know where it comes from
// and it seems to be harmless
return true;
}
else if (string.includes("script error")) {
message += 'No details available';
}
else {

View File

@@ -114,13 +114,14 @@ const contextMenuOptions = {
// Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
$tree.contextmenu("enableEntry", "delete", isNotRoot);
$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();

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,7 +11,7 @@ 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";
@@ -22,13 +23,12 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
$("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
$("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
$("#show-source-button").click(noteSourceDialog.showDialog);
utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
$("#recent-changes-button").click(recentChangesDialog.showDialog);
@@ -38,8 +38,8 @@ function registerEntrypoints() {
$("#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);
@@ -57,7 +57,13 @@ function registerEntrypoints() {
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 => {
@@ -109,27 +115,10 @@ function registerEntrypoints() {
$("#note-detail-text").focus();
});
$(document).bind('keydown', 'ctrl+-', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
if (webFrame.getZoomFactor() > 0.2) {
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
}
return false;
}
});
$(document).bind('keydown', 'ctrl+=', () => {
if (utils.isElectron()) {
const webFrame = require('electron').webFrame;
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
return false;
}
});
if (utils.isElectron()) {
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
}
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

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

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

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

@@ -39,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) {
@@ -80,11 +83,10 @@ async function setupProtectedSession() {
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
$protectedSessionOnButton.addClass('active');
$protectedSessionOffButton.removeClass('active');
protectedSessionDeferred = null;
}
}
@@ -105,6 +107,10 @@ async function enterProtectedSessionOnServer(password) {
}
async function protectNoteAndSendToServer() {
if (noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();
@@ -118,6 +124,10 @@ async function protectNoteAndSendToServer() {
}
async function unprotectNoteAndSendToServer() {
if (!noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote();

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

@@ -183,7 +183,7 @@ 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);
@@ -191,26 +191,21 @@ async function showParentList(noteId, node) {
$notePathCount.html(parents.length + " path" + (parents.length > 0 ? "s" : ""));
if (parents.length <= 1) {
}
else {
//$notePathList.show();
$notePathList.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);
const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
const item = $("<li/>").append(await linkService.createNoteLink(notePath, title));
if (node.getParent().data.noteId === parentNote.noteId) {
item.addClass("current");
}
$notePathList.append(item);
if (node.getParent().data.noteId === parentNote.noteId) {
item.addClass("current");
}
$notePathList.append(item);
}
}
@@ -323,7 +318,7 @@ function initFancyTree(tree) {
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),

View File

@@ -74,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
});

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

@@ -20,6 +20,7 @@
background-color: #f1f1f1;
display: flex;
align-items: center;
padding: 4px;
}
#note-detail-wrapper {
@@ -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;
}
@@ -66,31 +67,31 @@ ul.fancytree-container {
/* 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 {
@@ -106,7 +107,7 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
}
span.fancytree-node.tree-root > span.fancytree-icon {
background: url("../images/icons/tree-root.png") 0 0;
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 */
@@ -138,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;
}
@@ -170,11 +167,27 @@ div.ui-tooltip {
#tree {
overflow: auto;
flex-grow: 100;
flex-shrink: 100;
flex-grow: 1;
flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px;
}
#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;
}
/*
* .electron-in-page-search-window is a class specified to default
* <webview> element for search window.
@@ -232,7 +245,7 @@ div.ui-tooltip {
}
.suppressed {
filter: opacity(7%);
display: none;
}
#note-type .dropdown-menu li:not(.divider) {
@@ -241,7 +254,7 @@ div.ui-tooltip {
}
.dropdown-menu li:not(.divider):hover, .dropdown-menu li:not(.divider) a:hover {
background-color: #eee !important;
background-color: #ccc !important;
cursor: pointer;
}
@@ -359,14 +372,52 @@ div.ui-tooltip {
display: flex;
}
.btn {
border-color: #ddd;
.btn:not(.btn-primary):not(.btn-danger) {
border-color: #bbb;
background-color: #eee;
}
.btn.active {
background-color: #ddd;
.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

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

View File

@@ -21,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');

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

@@ -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
@@ -18,6 +18,12 @@ async function getRecentNotes() {
ORDER BY
dateCreated DESC
LIMIT 200`);
for (const rn of recentNotes) {
rn.title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
}
return recentNotes;
}
async function addRecentNote(req) {

View File

@@ -2,15 +2,69 @@
const sql = require('../../services/sql');
const noteService = require('../../services/notes');
const noteCacheService = require('../../services/note_cache');
const parseFilters = require('../../services/parse_filters');
const buildSearchQuery = require('../../services/build_search_query');
async function searchNotes(req) {
const {labelFilters, searchText} = parseFilters(req.params.searchString);
const {query, params} = buildSearchQuery(labelFilters, searchText);
let labelFiltersNoteIds = null;
const noteIds = await sql.getColumn(query, params);
if (labelFilters.length > 0) {
const {query, params} = buildSearchQuery(labelFilters, searchText);
labelFiltersNoteIds = await sql.getColumn(query, params);
}
let searchTextResults = null;
if (searchText.trim().length > 0) {
searchTextResults = noteCacheService.findNotes(searchText);
let fullTextNoteIds = await getFullTextResults(searchText);
for (const noteId of fullTextNoteIds) {
if (!searchTextResults.some(item => item.noteId === noteId)) {
const result = noteCacheService.getNotePath(noteId);
if (result) {
searchTextResults.push(result);
}
}
}
}
let results;
if (labelFiltersNoteIds && searchTextResults) {
results = labelFiltersNoteIds.filter(item => searchTextResults.includes(item.noteId));
}
else if (labelFiltersNoteIds) {
results = labelFiltersNoteIds.map(noteCacheService.getNotePath).filter(res => !!res);
}
else {
results = searchTextResults;
}
return results;
}
async function getFullTextResults(searchText) {
const tokens = searchText.toLowerCase().split(" ");
const tokenSql = ["1=1"];
for (const token of tokens) {
// FIXME: escape token!
tokenSql.push(`(title LIKE '%${token}%' OR content LIKE '%${token}%')`);
}
const noteIds = await sql.getColumn(`
SELECT DISTINCT noteId
FROM notes
WHERE isDeleted = 0
AND isProtected = 0
AND ${tokenSql.join(' AND ')}`);
return noteIds;
}
@@ -20,7 +74,7 @@ async function saveSearchToNote(req) {
searchString: req.params.searchString
};
const {note} = await noteService.createNote('root', 'Search note', noteContent, {
const {note} = await noteService.createNote('root', req.params.searchString, noteContent, {
json: true,
type: 'search',
mime: "application/json"

View File

@@ -4,9 +4,11 @@ const sourceIdService = require('../services/source_id');
const sql = require('../services/sql');
const labelService = require('../services/labels');
const config = require('../services/config');
const optionService = require('../services/options');
async function index(req, res) {
res.render('index', {
theme: await optionService.getOption('theme'),
sourceId: await sourceIdService.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
instanceName: config.General ? config.General.instanceName : null,

View File

@@ -1,9 +0,0 @@
"use strict";
function migrationPage(req, res) {
res.render('migration', {});
}
module.exports = {
migrationPage
};

View File

@@ -1,6 +1,5 @@
const indexRoute = require('./index');
const loginRoute = require('./login');
const migrationRoute = require('./migration');
const setupRoute = require('./setup');
const multer = require('multer')();
@@ -14,7 +13,6 @@ const noteRevisionsApiRoute = require('./api/note_revisions');
const recentChangesApiRoute = require('./api/recent_changes');
const optionsApiRoute = require('./api/options');
const passwordApiRoute = require('./api/password');
const migrationApiRoute = require('./api/migration');
const syncApiRoute = require('./api/sync');
const loginApiRoute = require('./api/login');
const eventLogRoute = require('./api/event_log');
@@ -96,7 +94,6 @@ function register(app) {
route(GET, '/login', [], loginRoute.loginPage);
route(POST, '/login', [], loginRoute.login);
route(POST, '/logout', [auth.checkAuth], loginRoute.logout);
route(GET, '/migration', [auth.checkAuthForMigrationPage], migrationRoute.migrationPage);
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
@@ -180,9 +177,6 @@ function register(app) {
apiRoute(GET, '/api/search/:searchString', searchRoute.searchNotes);
apiRoute(POST, '/api/search/:searchString', searchRoute.saveSearchToNote);
route(GET, '/api/migration', [auth.checkApiAuthForMigrationPage], migrationApiRoute.getMigrationInfo, apiResultHandler);
route(POST, '/api/migration', [auth.checkApiAuthForMigrationPage], migrationApiRoute.executeMigration, apiResultHandler);
route(POST, '/api/login/sync', [], loginApiRoute.loginSync, apiResultHandler);
// this is for entering protected mode so user has to be already logged-in (that's the reason we don't require username)
apiRoute(POST, '/api/login/protected', loginApiRoute.loginToProtectedSession);

Binary file not shown.

View File

@@ -3,11 +3,13 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 96;
const APP_DB_VERSION = 100;
const SYNC_VERSION = 1;
module.exports = {
appVersion: packageJson.version,
dbVersion: APP_DB_VERSION,
syncVersion: SYNC_VERSION,
buildDate: build.buildDate,
buildRevision: build.buildRevision
};

View File

@@ -12,18 +12,6 @@ async function checkAuth(req, res, next) {
else if (!req.session.loggedIn && !utils.isElectron()) {
res.redirect("login");
}
else if (!await sqlInit.isDbUpToDate()) {
res.redirect("migration");
}
else {
next();
}
}
async function checkAuthForMigrationPage(req, res, next) {
if (!req.session.loggedIn && !utils.isElectron()) {
res.redirect("login");
}
else {
next();
}
@@ -35,27 +23,12 @@ async function checkApiAuthOrElectron(req, res, next) {
if (!req.session.loggedIn && !utils.isElectron()) {
res.status(401).send("Not authorized");
}
else if (await sqlInit.isDbUpToDate()) {
next();
}
else {
res.status(409).send("Mismatched app versions"); // need better response than that
next();
}
}
async function checkApiAuth(req, res, next) {
if (!req.session.loggedIn) {
res.status(401).send("Not authorized");
}
else if (await sqlInit.isDbUpToDate()) {
next();
}
else {
res.status(409).send("Mismatched app versions"); // need better response than that
}
}
async function checkApiAuthForMigrationPage(req, res, next) {
if (!req.session.loggedIn) {
res.status(401).send("Not authorized");
}
@@ -79,19 +52,14 @@ async function checkSenderToken(req, res, next) {
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
res.status(401).send("Not authorized");
}
else if (await sqlInit.isDbUpToDate()) {
next();
}
else {
res.status(409).send("Mismatched app versions"); // need better response than that
next();
}
}
module.exports = {
checkAuth,
checkAuthForMigrationPage,
checkApiAuth,
checkApiAuthForMigrationPage,
checkAppNotInitialized,
checkApiAuthOrElectron,
checkSenderToken

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-05-31T23:23:44-04:00", buildRevision: "80d2457b23b916b869fe2a91ae4e65e33ab42549" };
module.exports = { buildDate:"2018-07-09T21:22:12+02:00", buildRevision: "14cffbbe625036d7284056f6a37a5e748e397148" };

View File

@@ -1,4 +1,4 @@
module.exports = function(labelFilters, searchText) {
module.exports = function(labelFilters) {
const joins = [];
const joinParams = [];
let where = '1';
@@ -44,16 +44,6 @@ module.exports = function(labelFilters, searchText) {
let searchCondition = '';
const searchParams = [];
if (searchText.trim() !== '') {
// searching in protected notes is pointless because of encryption
searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
searchText = '%' + searchText.trim() + '%';
searchParams.push(searchText);
searchParams.push(searchText); // two occurences in searchCondition
}
const query = `SELECT DISTINCT notes.noteId FROM notes
${joins.join('\r\n')}
WHERE

View File

@@ -16,8 +16,11 @@ const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
async function getHash(entityConstructor, whereBranch) {
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
// subselect is necessary to have correct ordering in GROUP_CONCAT
const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
let contentToHash = await sql.getValue(query);
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";

View File

@@ -6,7 +6,7 @@ const Label = require('../entities/label');
const BUILTIN_LABELS = [
'disableVersioning',
'calendarRoot',
'hideInAutocomplete',
'archived',
'excludeFromExport',
'run',
'manualTransactionHandling',

View File

@@ -8,8 +8,9 @@ const utils = require('./utils');
let noteTitles;
let protectedNoteTitles;
let noteIds;
let childParentToBranchId = {};
const childToParent = {};
const hideInAutocomplete = {};
const archived = {};
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
let prefixes = {};
@@ -20,21 +21,22 @@ async function load() {
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, prefix FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
const relations = await sql.getRows(`SELECT branchId, noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
for (const rel of relations) {
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
childToParent[rel.noteId].push(rel.parentNoteId);
childParentToBranchId[`${rel.noteId}-${rel.parentNoteId}`] = rel.branchId;
}
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'archived'`);
for (const noteId of hiddenLabels) {
hideInAutocomplete[noteId] = true;
archived[noteId] = true;
}
}
function getResults(query) {
function findNotes(query) {
if (!noteTitles || query.length <= 2) {
return [];
}
@@ -49,7 +51,7 @@ function getResults(query) {
}
for (const noteId of noteIds) {
if (hideInAutocomplete[noteId]) {
if (archived[noteId]) {
continue;
}
@@ -59,7 +61,7 @@ function getResults(query) {
}
for (const parentNoteId of parents) {
if (hideInAutocomplete[parentNoteId]) {
if (archived[parentNoteId]) {
continue;
}
@@ -91,8 +93,12 @@ function search(noteId, tokens, path, results) {
if (retPath) {
const noteTitle = getNoteTitleForPath(retPath);
const thisNoteId = retPath[retPath.length - 1];
const thisParentNoteId = retPath[retPath.length - 2];
results.push({
noteId: thisNoteId,
branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`],
title: noteTitle,
path: retPath.join('/')
});
@@ -111,7 +117,7 @@ function search(noteId, tokens, path, results) {
return;
}
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
if (parentNoteId === 'root' || archived[parentNoteId]) {
continue;
}
@@ -155,6 +161,15 @@ function getNoteTitle(noteId, parentNoteId) {
function getNoteTitleForPath(path) {
const titles = [];
if (path[0] === 'root') {
if (path.length === 1) {
return getNoteTitle('root');
}
else {
path = path.slice(1);
}
}
let parentNoteId = 'root';
for (const noteId of path) {
@@ -180,6 +195,10 @@ function getSomePath(noteId, path) {
}
for (const parentNoteId of parents) {
if (archived[parentNoteId]) {
continue;
}
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
if (retPath) {
@@ -190,6 +209,22 @@ function getSomePath(noteId, path) {
return false;
}
function getNotePath(noteId) {
const retPath = getSomePath(noteId, []);
if (retPath) {
const noteTitle = getNoteTitleForPath(retPath);
const parentNoteId = childToParent[noteId][0];
return {
noteId: noteId,
branchId: childParentToBranchId[`${noteId}-${parentNoteId}`],
title: noteTitle,
path: retPath.join('/')
};
}
}
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
if (entityName === 'notes') {
const note = await repository.getNote(entityId);
@@ -211,6 +246,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
if (branch.isDeleted) {
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
delete childParentToBranchId[branch.noteId + '-' + branch.parentNoteId];
}
else {
if (branch.prefix) {
@@ -219,21 +255,22 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
childToParent[branch.noteId].push(branch.parentNoteId);
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
}
}
else if (entityName === 'labels') {
const label = await repository.getLabel(entityId);
if (label.name === 'hideInAutocomplete') {
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
if (label.name === 'archived') {
// we're not using label object directly, since there might be other non-deleted archived label
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
AND name = 'archived' AND noteId = ?`, [label.noteId]);
if (hideLabel) {
hideInAutocomplete[label.noteId] = true;
archived[label.noteId] = true;
}
else {
delete hideInAutocomplete[label.noteId];
delete archived[label.noteId];
}
}
}
@@ -250,5 +287,7 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
module.exports = {
getResults
findNotes,
getNotePath,
getNoteTitleForPath
};

View File

@@ -2,7 +2,6 @@ const repository = require('./repository');
const utils = require('./utils');
const dateUtils = require('./date_utils');
const appInfo = require('./app_info');
const Option = require('../entities/option');
async function getOption(name) {
const option = await repository.getOption(name);
@@ -27,6 +26,9 @@ async function setOption(name, value) {
}
async function createOption(name, value, isSynced) {
// to avoid circular dependency, need to find better solution
const Option = require('../entities/option');
await new Option({
name: name,
value: value,
@@ -53,6 +55,9 @@ async function initOptions(startNotePath) {
await createOption('lastSyncedPull', appInfo.dbVersion, false);
await createOption('lastSyncedPush', 0, false);
await createOption('zoomFactor', 1.0, false);
await createOption('theme', 'white', false);
}
module.exports = {

View File

@@ -42,7 +42,10 @@ const dbReady = new Promise((resolve, reject) => {
}
if (!await isDbUpToDate()) {
return;
// avoiding circular dependency
const migrationService = require('./migration');
await migrationService.migrate();
}
resolve(db);
@@ -94,6 +97,12 @@ async function isDbUpToDate() {
}
async function isUserInitialized() {
const optionsTable = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
if (optionsTable.length !== 1) {
return false;
}
const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'");
return !!username;

View File

@@ -22,13 +22,6 @@ let syncServerCertificate = null;
async function sync() {
try {
await syncMutexService.doExclusively(async () => {
if (!await sqlInit.isDbUpToDate()) {
return {
success: false,
message: "DB not up to date"
};
}
const syncContext = await login();
await pushSync(syncContext);
@@ -76,7 +69,7 @@ async function login() {
const resp = await syncRequest(syncContext, 'POST', '/api/login/sync', {
timestamp: timestamp,
dbVersion: appInfo.dbVersion,
syncVersion: appInfo.syncVersion,
hash: hash
});
@@ -212,7 +205,7 @@ const primaryKeys = {
"note_images": "noteImageId",
"labels": "labelId",
"api_tokens": "apiTokenId",
"options": "optionId"
"options": "name"
};
async function getEntityRow(entityName, entityId) {

View File

@@ -79,6 +79,32 @@ function stripTags(text) {
return text.replace(/<(?:.|\n)*?>/gm, '');
}
function intersection(a, b) {
return a.filter(value => b.indexOf(value) !== -1);
}
function union(a, b) {
const obj = {};
for (let i = a.length-1; i >= 0; i--) {
obj[a[i]] = a[i];
}
for (let i = b.length-1; i >= 0; i--) {
obj[b[i]] = b[i];
}
const res = [];
for (const k in obj) {
if (obj.hasOwnProperty(k)) { // <-- optional
res.push(obj[k]);
}
}
return res;
}
module.exports = {
randomSecureToken,
randomString,
@@ -93,5 +119,7 @@ module.exports = {
stopWatch,
unescapeHtml,
toObject,
stripTags
stripTags,
intersection,
union
};

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="theme-<%= theme %>">
<head>
<meta charset="utf-8">
<title>Trilium Notes</title>
@@ -7,24 +7,18 @@
<body>
<div id="container" style="display:none;">
<div id="header" class="hide-toggle">
<div id="header-title">
<img src="/images/app-icons/png/24x24.png">
Trilium Notes
</div>
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action"
style="background: url('/images/icons/back.png')"></a>
style="background: url('/images/icons/back-24.png')"></a>
&nbsp; &nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action"
style="background: url('/images/icons/forward.png')"></a>
style="background: url('/images/icons/forward-24.png')"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-xs" id="jump-to-note-button" title="CTRL+J">Jump to note</button>
<button class="btn btn-xs" id="jump-to-note-dialog-button" title="CTRL+J">Jump to note</button>
<button class="btn btn-xs" id="recent-notes-button" title="CTRL+E">Recent notes</button>
<button class="btn btn-xs" id="recent-changes-button">Recent changes</button>
<div>
@@ -57,18 +51,18 @@
</div>
<div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 10px 0 16px; border: 1px solid #ccc;">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action"
style="background: url('/images/icons/file-plus.png')"></a>
style="background: url('/images/icons/file-plus-24.png')"></a>
<a id="collapse-tree-button" title="Collapse note tree" class="icon-action"
style="background: url('/images/icons/list.png')"></a>
style="background: url('/images/icons/list-24.png')"></a>
<a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
style="background: url('/images/icons/crosshair.png')"></a>
style="background: url('/images/icons/crosshair-24.png')"></a>
<a id="toggle-search-button" title="Search in notes" class="icon-action"
style="background: url('/images/icons/search.png')"></a>
style="background: url('/images/icons/search-24.png')"></a>
</div>
<input type="file" id="import-upload" style="display: none" />
@@ -76,22 +70,37 @@
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;">
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
</div>
<button id="do-search-button" class="btn btn-sm icon-button" title="Search (enter)"
style="background-image: url('/images/icons/search-20.png');"></button>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
&nbsp;
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
<button id="save-search-button" class="btn btn-sm icon-button" title="Save search"
style="background-image: url('/images/icons/save-20.png');"></button>
&nbsp;
<button id="close-search-button" class="btn btn-sm icon-button" title="Close search"
style="background-image: url('/images/icons/x-20.png');"></button>
</div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<ul id="search-results-inner">
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
</ul>
</div>
<div id="tree"></div>
</div>
<div style="grid-area: title;">
<div class="hide-toggle" style="display: flex; align-items: center;">
<div class="dropdown">
<div style="display: flex; align-items: center;">
<div class="dropdown hide-toggle">
<button id="note-path-list-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span id="note-path-count">1 path</span>
@@ -103,60 +112,69 @@
<input autocomplete="off" value="" id="note-title" style="margin-left: 15px; font-size: x-large; border: 0; flex-grow: 100;" tabindex="1">
<span id="note-id-display" title="Note ID"></span>
<div class="hide-toggle" style="display: flex; align-items: center;">
<span id="note-id-display" title="Note ID"></span>
<a title="Protect the note so that password will be required to view the note"
class="icon-action"
id="protect-button"
style="display: none; background: url('images/icons/lock.png')"></a>
<button class="btn btn-sm icon-button"
style="display: none; margin-right: 10px; background-image: url('/images/icons/edit-20.png');"
title="Toggle edit"
id="toggle-edit-button"></button>
<a title="Unprotect note so that password will not be required to access this note in the future"
class="icon-action"
id="unprotect-button"
style="display: none; background: url('images/icons/unlock.png')"></a>
<button class="btn btn-sm icon-button"
style="display: none; margin-right: 10px; background-image: url('/images/icons/play-20.png');"
title="Render (Ctrl+Enter)"
id="render-button"></button>
&nbsp; &nbsp;
<button class="btn btn-sm icon-button"
style="display: none; margin-right: 10px; background-image: url('/images/icons/play-20.png');"
title="Execute (Ctrl+Enter)"
id="execute-script-button"></button>
<button class="btn btn-sm"
style="display: none; margin-right: 10px"
id="toggle-edit-button">Toggle edit</button>
<div>
<button type="button"
class="btn btn-sm icon-button"
id="protect-button"
title="Protected note can be viewed and edited only after entering password"
style="background-image: url('/images/icons/shield-20.png');">
</button><button type="button"
class="btn btn-sm icon-button"
id="unprotect-button"
title="Not protected note can be viewed without entering password"
style="background-image: url('/images/icons/shield-off-20.png');">
</button>
</div>
<button class="btn btn-sm"
style="display: none; margin-right: 10px"
id="render-button">Render <kbd>Ctrl+Enter</kbd></button>
&nbsp; &nbsp;
<button class="btn btn-sm"
style="display: none; margin-right: 10px"
id="execute-script-button">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type" data-bind="visible: type() != 'search'">
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
<ul id="note-type-dropdown" class="dropdown-menu dropdown-menu-right">
<li data-bind="click: selectText, css: { selected: type() == 'text' }"><span class="check">&check;</span> <strong>Text</strong></li>
<li role="separator" class="divider"></li>
<li data-bind="click: selectRender, css: { selected: type() == 'render' && mime() == '' }"><span class="check">&check;</span> <strong>Render HTML note</strong></li>
<li role="separator" class="divider"></li>
<li data-bind="click: selectCode, css: { selected: type() == 'code' && mime() == '' }"><span class="check">&check;</span> <strong>Code</strong></li>
<!-- ko foreach: codeMimeTypes -->
<li data-bind="click: $parent.selectCodeMime, css: { selected: $parent.type() == 'code' && $parent.mime() == mime }"><span class="check">&check;</span> <span data-bind="text: title"></span></li>
<!-- /ko -->
</ul>
</div>
<div class="dropdown" id="note-type" data-bind="visible: type() != 'search'">
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
<ul id="note-type-dropdown" class="dropdown-menu dropdown-menu-right">
<li data-bind="click: selectText, css: { selected: type() == 'text' }"><span class="check">&check;</span> <strong>Text</strong></li>
<li role="separator" class="divider"></li>
<li data-bind="click: selectRender, css: { selected: type() == 'render' && mime() == '' }"><span class="check">&check;</span> <strong>Render HTML note</strong></li>
<li role="separator" class="divider"></li>
<li data-bind="click: selectCode, css: { selected: type() == 'code' && mime() == '' }"><span class="check">&check;</span> <strong>Code</strong></li>
<!-- ko foreach: codeMimeTypes -->
<li data-bind="click: $parent.selectCodeMime, css: { selected: $parent.type() == 'code' && $parent.mime() == mime }"><span class="check">&check;</span> <span data-bind="text: title"></span></li>
<!-- /ko -->
</ul>
</div>
<div class="dropdown" style="margin-left: 10px; margin-right: 10px;">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Note actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a id="show-note-revisions-button">Note revisions</a></li>
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
<li><a id="show-source-button"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a id="upload-file-button">Upload file</a></li>
</ul>
<div class="dropdown" id="note-actions">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Note actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a id="show-note-revisions-button">Note revisions</a></li>
<li><a class="show-labels-button"><kbd>Alt+L</kbd> Labels</a></li>
<li><a id="show-source-button">HTML source</a></li>
<li><a id="upload-file-button">Upload file</a></li>
</ul>
</div>
</div>
</div>
</div>
@@ -248,7 +266,7 @@
<input id="recent-notes-search-input" class="form-control"/>
</div>
<div id="add-link-dialog" title="Add link" style="display: none;">
<div id="add-link-dialog" title="Add note link" style="display: none;">
<form id="add-link-form">
<div id="add-link-type-div" class="radio">
<label title="Add HTML link to the selected note at cursor in current note">
@@ -266,7 +284,7 @@
<div class="form-group">
<label for="note-autocomplete">Note</label>
<input id="note-autocomplete" style="width: 100%;">
<input id="note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
</div>
<div class="form-group" id="add-link-title-form-group">
@@ -279,7 +297,7 @@
<input id="clone-prefix" style="width: 100%;">
</div>
<button class="btn btn-sm">Add link</button>
<button class="btn btn-sm">Add note link</button>
</form>
</div>
@@ -287,10 +305,14 @@
<form id="jump-to-note-form">
<div class="form-group">
<label for="jump-to-note-autocomplete">Note</label>
<input id="jump-to-note-autocomplete" style="width: 100%;">
<input id="jump-to-note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
</div>
<button name="action" value="jump" class="btn btn-sm">Jump <kbd>enter</kbd></button>
<div style="display: flex; justify-content: space-between;">
<button id="jump-to-note-button" class="btn btn-sm btn-primary">Jump <kbd>enter</kbd></button>
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
</div>
</form>
</div>
@@ -308,12 +330,35 @@
<div id="options-dialog" title="Options" style="display: none;">
<div id="options-tabs">
<ul>
<li><a href="#appearance">Apperance</a></li>
<li><a href="#change-password">Change password</a></li>
<li><a href="#protected-session-timeout">Protected session</a></li>
<li><a href="#note-revision-snapshot-time-interval">Note revisions</a></li>
<li><a href="#advanced">Advanced</a></li>
<li><a href="#about">About Trilium</a></li>
</ul>
<div id="appearance">
<p>Settings on this options tab are saved automatically after each change.</p>
<form>
<div class="form-group">
<label for="theme-select">Theme</label>
<select class="form-control" id="theme-select">
<option value="white">White</option>
<option value="dark">Dark</option>
<option value="black">Black</option>
</select>
</div>
<div class="form-group">
<label for="zoom-factor-select">Zoom factor (desktop build only)</label>
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p>
</form>
</div>
<div id="change-password">
<form id="change-password-form">
<div class="form-group">
@@ -402,12 +447,14 @@
<th>App version:</th>
<td id="app-version"></td>
</tr>
<tr>
<th>DB version:</th>
<td id="db-version"></td>
</tr>
<tr>
<th>Sync version:</th>
<td id="sync-version"></td>
</tr>
<tr>
<th>Build date:</th>
<td id="build-date"></td>

View File

@@ -1,72 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Migration</title>
</head>
<body>
<div style="width: 800px; margin: auto;">
<h1>Migration</h1>
<div id="up-to-date" style="display:none;">
<p>Your database is up-to-date with the application.</p>
<a href="/" class="btn btn-success">Continue to app</a>
</div>
<div id="need-to-migrate" style="display:none;">
<p>Your database needs to be migrated to new version before you can use the application again.
Database will be backed up before migration in case of something going wrong.</p>
<table class="table table-bordered" style="width: 200px;">
<tr>
<th>Application version:</th>
<td id="app-db-version" style="text-align: right;"></td>
<tr>
<th>Database version:</th>
<td id="db-version" style="text-align: right;"></td>
</tr>
</table>
<button class="btn btn-warning" id="run-migration">Run migration</button>
</div>
<div id="migration-result" style="display:none;">
<h2>Migration result</h2>
<table id="migration-table" class="table">
<tr>
<th>Database version</th>
<th>Name</th>
<th>Success</th>
<th>Error</th>
</tr>
</table>
<a href="/" class="btn btn-success">Continue to app</a>
</div>
</div>
<script type="text/javascript">
const baseApiUrl = 'api/';
</script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {
window.module = module; module = undefined;
}
const glob = {
sourceId: ''
};
</script>
<script src="libraries/jquery.min.js"></script>
<link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet">
<script src="libraries/bootstrap/js/bootstrap.js"></script>
<script src="javascripts/migration.js" type="module"></script>
</body>
</html>

View File

@@ -18,6 +18,7 @@ const log = require('./services/log');
const appInfo = require('./services/app_info');
const messagingService = require('./services/messaging');
const utils = require('./services/utils');
const sqlInit = require('./services/sql_init.js');
const port = normalizePort(config['Network']['port'] || '3000');
app.set('port', port);
@@ -54,7 +55,7 @@ httpServer.listen(port);
httpServer.on('error', onError);
httpServer.on('listening', onListening);
messagingService.init(httpServer, sessionParser);
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
if (utils.isElectron()) {
const electronRouting = require('./routes/electron');