Compare commits
	
		
			37 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7a9542b4fc | ||
|  | 3a95c9e1bc | ||
|  | 3d2ef6be01 | ||
|  | d67246699a | ||
|  | 14c704d6db | ||
|  | 4c8eeb2e6f | ||
|  | c1b245c8b1 | ||
|  | 74202d67bb | ||
|  | 26066f39b1 | ||
|  | b255cf190c | ||
|  | bc77b143b0 | ||
|  | 9f0ff6ae7a | ||
|  | 736704c7d6 | ||
|  | 654c116c58 | ||
|  | 89a5cab98f | ||
|  | c39d0be8cd | ||
|  | e75b4cd848 | ||
|  | 378e8f35e5 | ||
|  | bdb5e2f13f | ||
|  | 8211bed449 | ||
|  | b243632483 | ||
|  | e4d2513451 | ||
|  | 385144451b | ||
|  | c8c533844e | ||
|  | 0e69f0c079 | ||
|  | aee60c444f | ||
|  | e7a504c66b | ||
|  | 45d9c7164c | ||
|  | bd913a63a8 | ||
|  | 5a1938c078 | ||
|  | 015cd68756 | ||
|  | 76c0e5b2b8 | ||
|  | 0f8f707acd | ||
|  | 083cccea28 | ||
|  | 31b76b23ce | ||
|  | af529f82e5 | ||
|  | fc6669d254 | 
							
								
								
									
										4
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| node_modules | ||||
| npm-debug.log | ||||
| dist | ||||
| .idea | ||||
| @@ -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>""</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>''</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>""</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>""</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>''</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>''</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>""</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>"unnamed"</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>""</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>'text'</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>'text/html'</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>""</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>""</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>'1970-01-01T00:00:00.000Z'</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>""</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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| if [[ $# -eq 0 ]] ; then | ||||
|     echo "Missing argument of new version" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| docker build -t zadam/trilium:latest -t zadam/trilium:$1 . | ||||
							
								
								
									
										9
									
								
								bin/push-docker-image.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,9 @@ | ||||
| #!/usr/bin/env bash | ||||
|  | ||||
| if [[ $# -eq 0 ]] ; then | ||||
|     echo "Missing argument of new version" | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| docker push zadam/trilium:latest | ||||
| docker push zadam/trilium:$1 | ||||
| @@ -75,4 +75,8 @@ github-release upload \ | ||||
|     --name "$WINDOWS_X64_BUILD" \ | ||||
|     --file "dist/$WINDOWS_X64_BUILD" | ||||
|  | ||||
| bin/build-docker.sh $VERSION | ||||
|  | ||||
| bin/push-docker-image.sh $VERSION | ||||
|  | ||||
| echo "Release finished!" | ||||
							
								
								
									
										2
									
								
								db/migrations/0097__add_zoomFactor.sql
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										1
									
								
								db/migrations/0098__rename_hideInAutocomplete.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete' | ||||
							
								
								
									
										2
									
								
								db/migrations/0099__add_theme_option.sql
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										15
									
								
								db/migrations/0100__remove_optionId.sql
									
									
									
									
									
										Normal 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; | ||||
| @@ -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 | ||||
| ); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.14.0", | ||||
|   "version": "0.16.0", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
|   | ||||
| @@ -21,11 +21,8 @@ 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); | ||||
|     } | ||||
|     } | ||||
|  | ||||
|     async save() { | ||||
|         await repository.updateEntity(this); | ||||
|   | ||||
| Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B | 
| Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B | 
| Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B | 
| Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 463 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/edit-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 312 B | 
| Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 288 B | 
| Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 284 B | 
| Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B | 
| Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B | 
| Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 155 B | 
| Before Width: | Height: | Size: 323 B | 
| Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 358 B | 
| Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 252 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 288 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/save-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 388 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/search-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 431 B | 
| Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B | 
| Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/shield-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 388 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/shield-off-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 462 B | 
| Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B | 
| Before Width: | Height: | Size: 337 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/x-20.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 259 B | 
| @@ -57,7 +57,15 @@ async function showDialog() { | ||||
|         source: async function(request, response) { | ||||
|             const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term)); | ||||
|  | ||||
|             if (result.length > 0) { | ||||
|                 response(result); | ||||
|             } | ||||
|             else { | ||||
|                 response([{ | ||||
|                     label: "No results", | ||||
|                     value: "No results" | ||||
|                 }]); | ||||
|             } | ||||
|         }, | ||||
|         minLength: 2, | ||||
|         change: async () => { | ||||
|   | ||||
| @@ -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)); | ||||
|  | ||||
|             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 | ||||
| }; | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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; | ||||
| }); | ||||
							
								
								
									
										2
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -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'; | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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,7 +23,7 @@ function registerEntrypoints() { | ||||
|  | ||||
|     utils.bindShortcut('ctrl+l', addLinkDialog.showDialog); | ||||
|  | ||||
|     $("#jump-to-note-button").click(jumpToNoteDialog.showDialog); | ||||
|     $("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog); | ||||
|     utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog); | ||||
|  | ||||
|     $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions); | ||||
| @@ -38,8 +39,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 +58,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,28 +116,11 @@ 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); | ||||
|         $(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor); | ||||
|         $(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor); | ||||
|     } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     $(document).bind('keydown', 'ctrl+=', () => { | ||||
|         if (utils.isElectron()) { | ||||
|             const webFrame = require('electron').webFrame; | ||||
|  | ||||
|             webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     $("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus()); | ||||
|  | ||||
|     $("#upload-file-button").click(fileService.uploadFile); | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										9
									
								
								src/public/javascripts/services/options_init.js
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 toggleSearch() { | ||||
|     if ($searchBox.is(":hidden")) { | ||||
| function showSearch() { | ||||
|     $searchBox.show(); | ||||
|     $searchInput.focus(); | ||||
| } | ||||
|     else { | ||||
| 
 | ||||
| function hideSearch() { | ||||
|     resetSearch(); | ||||
| 
 | ||||
|     $searchResults.hide(); | ||||
|     $searchBox.hide(); | ||||
| } | ||||
| 
 | ||||
| function toggleSearch() { | ||||
|     if ($searchBox.is(":hidden")) { | ||||
|         showSearch(); | ||||
|     } | ||||
|     else { | ||||
|         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 | ||||
| }; | ||||
| @@ -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) {} | ||||
|   | ||||
| @@ -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,10 +191,6 @@ async function showParentList(noteId, node) { | ||||
|  | ||||
|     $notePathCount.html(parents.length + " path" + (parents.length > 0 ? "s" : "")); | ||||
|  | ||||
|     if (parents.length <= 1) { | ||||
|     } | ||||
|     else { | ||||
|         //$notePathList.show(); | ||||
|     $notePathList.empty(); | ||||
|  | ||||
|     for (const parentNote of parents) { | ||||
| @@ -212,7 +208,6 @@ async function showParentList(noteId, node) { | ||||
|         $notePathList.append(item); | ||||
|     } | ||||
| } | ||||
| } | ||||
|  | ||||
| async function getSomeNotePath(note) { | ||||
|     utils.assertArguments(note); | ||||
| @@ -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), | ||||
|   | ||||
| @@ -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 | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -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('/')) { | ||||
|   | ||||
							
								
								
									
										51
									
								
								src/public/javascripts/services/zoom.js
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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 | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|     let labelFiltersNoteIds = null; | ||||
|  | ||||
|     if (labelFilters.length > 0) { | ||||
|         const {query, params} = buildSearchQuery(labelFilters, searchText); | ||||
|  | ||||
|     const noteIds = await sql.getColumn(query, params); | ||||
|         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" | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| function migrationPage(req, res) { | ||||
|     res.render('migration', {}); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     migrationPage | ||||
| }; | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2018-05-31T23:23:44-04:00", buildRevision: "80d2457b23b916b869fe2a91ae4e65e33ab42549" }; | ||||
| module.exports = { buildDate:"2018-06-16T13:34:39-04:00", buildRevision: "3a95c9e1bc4d608a2135c9412732c2c7fbb2834c" }; | ||||
|   | ||||
| @@ -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  | ||||
|   | ||||
| @@ -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 = ""; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ const Label = require('../entities/label'); | ||||
| const BUILTIN_LABELS = [ | ||||
|     'disableVersioning', | ||||
|     'calendarRoot', | ||||
|     'hideInAutocomplete', | ||||
|     'archived', | ||||
|     'excludeFromExport', | ||||
|     'run', | ||||
|     'manualTransactionHandling', | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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> | ||||
|  | ||||
|               | ||||
|  | ||||
|           <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> | ||||
|             <button id="do-search-button" class="btn btn-sm icon-button" title="Search (enter)" | ||||
|                     style="background-image: url('/images/icons/search-20.png');"></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> | ||||
|  | ||||
|               | ||||
|  | ||||
|             <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 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> | ||||
|         <div id="search-results"> | ||||
|           <strong>Search results:</strong> | ||||
|  | ||||
|             <button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button> | ||||
|           </div> | ||||
|           <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,32 +112,40 @@ | ||||
|  | ||||
|           <input autocomplete="off" value="" id="note-title" style="margin-left: 15px; font-size: x-large; border: 0; flex-grow: 100;" tabindex="1"> | ||||
|  | ||||
|           <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" | ||||
|             <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> | ||||
|  | ||||
|             <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> | ||||
|  | ||||
|             <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" | ||||
|              style="display: none; background: url('images/icons/unlock.png')"></a> | ||||
|                       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="toggle-edit-button">Toggle edit</button> | ||||
|  | ||||
|           <button class="btn btn-sm" | ||||
|                   style="display: none; margin-right: 10px" | ||||
|                   id="render-button">Render <kbd>Ctrl+Enter</kbd></button> | ||||
|  | ||||
|           <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> | ||||
| @@ -146,7 +163,7 @@ | ||||
|               </ul> | ||||
|             </div> | ||||
|  | ||||
|           <div class="dropdown" style="margin-left: 10px; margin-right: 10px;"> | ||||
|             <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> | ||||
| @@ -160,6 +177,7 @@ | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div id="note-detail-wrapper"> | ||||
|         <div id="note-detail-component-wrapper"> | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
							
								
								
									
										3
									
								
								src/www
									
									
									
									
									
								
							
							
						
						| @@ -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'); | ||||
|   | ||||