mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 03:46:37 +01:00 
			
		
		
		
	Compare commits
	
		
			194 Commits
		
	
	
		
			v0.7.0-bet
			...
			v0.11.0-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2dc16dd29f | ||
| 
						 | 
					d8924c536b | ||
| 
						 | 
					3ebbf2cc46 | ||
| 
						 | 
					f4079604c9 | ||
| 
						 | 
					1f96a6beab | ||
| 
						 | 
					b277a250e5 | ||
| 
						 | 
					5b0e1a644d | ||
| 
						 | 
					6bb3cfa9a3 | ||
| 
						 | 
					9720868f5a | ||
| 
						 | 
					8d8ee2a87a | ||
| 
						 | 
					542e82ee5d | ||
| 
						 | 
					0104b19502 | ||
| 
						 | 
					120888b53e | ||
| 
						 | 
					d2e2caed62 | ||
| 
						 | 
					63066802a8 | ||
| 
						 | 
					6128bb4ff3 | ||
| 
						 | 
					982796255d | ||
| 
						 | 
					36b15f474d | ||
| 
						 | 
					13f71f8967 | ||
| 
						 | 
					64336ffbee | ||
| 
						 | 
					b09463d1b2 | ||
| 
						 | 
					b5e6f46b9c | ||
| 
						 | 
					08af4a0465 | ||
| 
						 | 
					8c5df6321f | ||
| 
						 | 
					d19f044961 | ||
| 
						 | 
					e378d9f645 | ||
| 
						 | 
					39dc0f71b4 | ||
| 
						 | 
					0cef5c6b8c | ||
| 
						 | 
					9b5a44cef4 | ||
| 
						 | 
					29769ed91d | ||
| 
						 | 
					867d794e17 | ||
| 
						 | 
					fdd8458336 | ||
| 
						 | 
					a0bec22e96 | ||
| 
						 | 
					5aeb5cd214 | ||
| 
						 | 
					e827ddffb9 | ||
| 
						 | 
					98f80998b9 | ||
| 
						 | 
					69727d0b12 | ||
| 
						 | 
					84faf32b98 | ||
| 
						 | 
					6ed6e27602 | ||
| 
						 | 
					fb54678fef | ||
| 
						 | 
					2cdcb3af12 | ||
| 
						 | 
					cf7a336ac2 | ||
| 
						 | 
					abfc64af95 | ||
| 
						 | 
					42dd8d4754 | ||
| 
						 | 
					a4e64350e9 | ||
| 
						 | 
					6f567e3e10 | ||
| 
						 | 
					c6c76ba360 | ||
| 
						 | 
					429d3f518e | ||
| 
						 | 
					26e4ad9bf9 | ||
| 
						 | 
					6ab0cea4e3 | ||
| 
						 | 
					277368ab43 | ||
| 
						 | 
					e2921a648d | ||
| 
						 | 
					c765dbc5cf | ||
| 
						 | 
					a066c6fe2b | ||
| 
						 | 
					311952d4dd | ||
| 
						 | 
					96dab5d51e | ||
| 
						 | 
					15d951b04e | ||
| 
						 | 
					8ba830c04b | ||
| 
						 | 
					acc82f39c4 | ||
| 
						 | 
					fad0ec757b | ||
| 
						 | 
					c9d73c6115 | ||
| 
						 | 
					ab2f28ceef | ||
| 
						 | 
					87e415992c | ||
| 
						 | 
					12439d8761 | ||
| 
						 | 
					4f200c73dc | ||
| 
						 | 
					5f7e74e15c | ||
| 
						 | 
					e8a5d0ae16 | ||
| 
						 | 
					088fb00ca9 | ||
| 
						 | 
					05676f3459 | ||
| 
						 | 
					5d203b2278 | ||
| 
						 | 
					795d50f02e | ||
| 
						 | 
					cfe0ae1eda | ||
| 
						 | 
					aa57a64c61 | ||
| 
						 | 
					e36a81e189 | ||
| 
						 | 
					88c07a9e48 | ||
| 
						 | 
					bfd9f292a6 | ||
| 
						 | 
					9edee9340b | ||
| 
						 | 
					8550ed72f2 | ||
| 
						 | 
					efffc29649 | ||
| 
						 | 
					0ec909fd7a | ||
| 
						 | 
					b10b0048f3 | ||
| 
						 | 
					9bb188b519 | ||
| 
						 | 
					7464835058 | ||
| 
						 | 
					913b6bb6f6 | ||
| 
						 | 
					000cf99546 | ||
| 
						 | 
					c918267750 | ||
| 
						 | 
					68921ee59b | ||
| 
						 | 
					7e856283ee | ||
| 
						 | 
					9c1b8da573 | ||
| 
						 | 
					cb39b9cca8 | ||
| 
						 | 
					788ac43ad1 | ||
| 
						 | 
					57d19f3302 | ||
| 
						 | 
					68bba623b6 | ||
| 
						 | 
					35998058ce | ||
| 
						 | 
					cdf94181d2 | ||
| 
						 | 
					91ee90d827 | ||
| 
						 | 
					d3316cd09c | ||
| 
						 | 
					ac1b06967f | ||
| 
						 | 
					47eb1e3e02 | ||
| 
						 | 
					a69d8737ce | ||
| 
						 | 
					341f47f0f2 | ||
| 
						 | 
					19c605a9a8 | ||
| 
						 | 
					54e4f54678 | ||
| 
						 | 
					297a2cd9da | ||
| 
						 | 
					d746d707b5 | ||
| 
						 | 
					299252b650 | ||
| 
						 | 
					fddd1c278f | ||
| 
						 | 
					f52d7e3c28 | ||
| 
						 | 
					a699210a29 | ||
| 
						 | 
					b3c32a39e9 | ||
| 
						 | 
					df27533b66 | ||
| 
						 | 
					b96a1274c5 | ||
| 
						 | 
					001a5107dd | ||
| 
						 | 
					c8e456cdb1 | ||
| 
						 | 
					0f6b00e1c8 | ||
| 
						 | 
					5ea060a054 | ||
| 
						 | 
					95bb2cf0bb | ||
| 
						 | 
					4c472ce78b | ||
| 
						 | 
					511fb89af0 | ||
| 
						 | 
					7e524c0cd1 | ||
| 
						 | 
					e3e2dc9fff | ||
| 
						 | 
					1612e9093d | ||
| 
						 | 
					f8649feea4 | ||
| 
						 | 
					ac978c3fa7 | ||
| 
						 | 
					efcc804149 | ||
| 
						 | 
					db514e8f41 | ||
| 
						 | 
					9c32f66329 | ||
| 
						 | 
					0fd5102a26 | ||
| 
						 | 
					840af15dae | ||
| 
						 | 
					f1b0b3bcdb | ||
| 
						 | 
					e5c0acbb43 | ||
| 
						 | 
					834661c461 | ||
| 
						 | 
					5204ab5a7e | ||
| 
						 | 
					74862536a8 | ||
| 
						 | 
					a24f1f5b95 | ||
| 
						 | 
					0be76f746a | ||
| 
						 | 
					fad89ff63f | ||
| 
						 | 
					b8ae791191 | ||
| 
						 | 
					ce754cbd91 | ||
| 
						 | 
					0fdb6af98a | ||
| 
						 | 
					f6c7f6a0f2 | ||
| 
						 | 
					354999f37a | ||
| 
						 | 
					348c622845 | ||
| 
						 | 
					44bcdedaba | ||
| 
						 | 
					755c0f3ce2 | ||
| 
						 | 
					895bda41b5 | ||
| 
						 | 
					b2df622cb6 | ||
| 
						 | 
					9ba6e6d0f5 | ||
| 
						 | 
					a5c9180533 | ||
| 
						 | 
					e86f1e0d05 | ||
| 
						 | 
					b6277049f3 | ||
| 
						 | 
					c831221cc4 | ||
| 
						 | 
					577a168714 | ||
| 
						 | 
					b0bd27321a | ||
| 
						 | 
					90c5348ca7 | ||
| 
						 | 
					8e95b080da | ||
| 
						 | 
					766a567a32 | ||
| 
						 | 
					6d0218cb36 | ||
| 
						 | 
					d26170762b | ||
| 
						 | 
					b3209a9bbf | ||
| 
						 | 
					61c2456cf6 | ||
| 
						 | 
					1c6fc9029f | ||
| 
						 | 
					5c91e38dfe | ||
| 
						 | 
					07bf075894 | ||
| 
						 | 
					ddce5c959e | ||
| 
						 | 
					3b9d1df05c | ||
| 
						 | 
					d239ef2956 | ||
| 
						 | 
					7a865a9081 | ||
| 
						 | 
					83d6c2970f | ||
| 
						 | 
					8c7d159012 | ||
| 
						 | 
					d169f67901 | ||
| 
						 | 
					982b723647 | ||
| 
						 | 
					31d5ac05ff | ||
| 
						 | 
					72d91d1571 | ||
| 
						 | 
					f4b57f4c57 | ||
| 
						 | 
					ee0833390a | ||
| 
						 | 
					2acff07368 | ||
| 
						 | 
					bea1d24f07 | ||
| 
						 | 
					adc270c59f | ||
| 
						 | 
					66064f7a94 | ||
| 
						 | 
					1501fa8dbf | ||
| 
						 | 
					60bba46d80 | ||
| 
						 | 
					12c06ae97e | ||
| 
						 | 
					f0bea9cf71 | ||
| 
						 | 
					a555b6319c | ||
| 
						 | 
					5dd93e4cdc | ||
| 
						 | 
					3b4509d833 | ||
| 
						 | 
					19308bbfbd | ||
| 
						 | 
					4acc5432c3 | ||
| 
						 | 
					08b8141fdf | ||
| 
						 | 
					e1200aa308 | ||
| 
						 | 
					89666eb078 | ||
| 
						 | 
					d5605aa64d | ||
| 
						 | 
					2582b016f9 | 
							
								
								
									
										14
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
 | 
			
		||||
    <data-source source="LOCAL" name="document.db" uuid="a2c75661-f9e2-478f-a69f-6a9409e69997">
 | 
			
		||||
      <driver-ref>sqlite.xerial</driver-ref>
 | 
			
		||||
      <synchronize>true</synchronize>
 | 
			
		||||
      <jdbc-driver>org.sqlite.JDBC</jdbc-driver>
 | 
			
		||||
      <jdbc-url>jdbc:sqlite:$USER_HOME$/trilium-data/document.db</jdbc-url>
 | 
			
		||||
      <driver-properties>
 | 
			
		||||
        <property name="enable_load_extension" value="true" />
 | 
			
		||||
      </driver-properties>
 | 
			
		||||
    </data-source>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										588
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										588
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,588 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<dataSource name="document.db">
 | 
			
		||||
  <database-model serializer="dbm" rdbms="SQLITE" format-version="4.7">
 | 
			
		||||
    <root id="1"/>
 | 
			
		||||
    <schema id="2" parent="1" name="main">
 | 
			
		||||
      <Current>1</Current>
 | 
			
		||||
      <Visible>1</Visible>
 | 
			
		||||
    </schema>
 | 
			
		||||
    <collation id="3" parent="1" name="BINARY"/>
 | 
			
		||||
    <collation id="4" parent="1" name="NOCASE"/>
 | 
			
		||||
    <collation id="5" parent="1" name="RTRIM"/>
 | 
			
		||||
    <table id="6" parent="2" name="api_tokens"/>
 | 
			
		||||
    <table id="7" parent="2" name="branches"/>
 | 
			
		||||
    <table id="8" parent="2" name="event_log"/>
 | 
			
		||||
    <table id="9" parent="2" name="images"/>
 | 
			
		||||
    <table id="10" parent="2" name="labels"/>
 | 
			
		||||
    <table id="11" parent="2" name="note_images"/>
 | 
			
		||||
    <table id="12" parent="2" name="note_revisions"/>
 | 
			
		||||
    <table id="13" parent="2" name="notes"/>
 | 
			
		||||
    <table id="14" parent="2" name="options"/>
 | 
			
		||||
    <table id="15" parent="2" name="recent_notes"/>
 | 
			
		||||
    <table id="16" parent="2" name="source_ids"/>
 | 
			
		||||
    <table id="17" parent="2" name="sqlite_master">
 | 
			
		||||
      <System>1</System>
 | 
			
		||||
    </table>
 | 
			
		||||
    <table id="18" parent="2" name="sqlite_sequence">
 | 
			
		||||
      <System>1</System>
 | 
			
		||||
    </table>
 | 
			
		||||
    <table id="19" parent="2" name="sync"/>
 | 
			
		||||
    <column id="20" parent="6" name="apiTokenId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="21" parent="6" name="token">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="22" parent="6" name="dateCreated">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="23" parent="6" name="isDeleted">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="24" parent="6" name="sqlite_autoindex_api_tokens_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>apiTokenId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="25" parent="6">
 | 
			
		||||
      <ColNames>apiTokenId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_api_tokens_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="26" parent="7" name="branchId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="27" parent="7" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="28" parent="7" name="parentNoteId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="29" parent="7" name="notePosition">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="30" parent="7" name="prefix">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="31" parent="7" name="isExpanded">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>BOOLEAN|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="32" parent="7" name="isDeleted">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="33" parent="7" name="dateModified">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="34" parent="7" name="sqlite_autoindex_branches_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="35" parent="7" name="IDX_branches_noteId_parentNoteId">
 | 
			
		||||
      <ColNames>noteId
 | 
			
		||||
parentNoteId</ColNames>
 | 
			
		||||
      <ColumnCollations>
 | 
			
		||||
</ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="36" parent="7" name="IDX_branches_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="37" parent="7">
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_branches_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="38" parent="8" name="id">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <SequenceIdentity>1</SequenceIdentity>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="39" parent="8" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="40" parent="8" name="comment">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="41" parent="8" name="dateAdded">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <key id="42" parent="8">
 | 
			
		||||
      <ColNames>id</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
    </key>
 | 
			
		||||
    <foreign-key id="43" parent="8">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <RefTableName>notes</RefTableName>
 | 
			
		||||
      <RefColNames>noteId</RefColNames>
 | 
			
		||||
    </foreign-key>
 | 
			
		||||
    <column id="44" parent="9" name="imageId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="45" parent="9" name="format">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="46" parent="9" name="checksum">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="47" parent="9" name="name">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="48" parent="9" name="data">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>BLOB|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="49" parent="9" name="isDeleted">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="50" parent="9" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="51" parent="9" name="dateCreated">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="52" parent="9" name="sqlite_autoindex_images_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="53" parent="9">
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_images_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="54" parent="10" name="labelId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="55" parent="10" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="56" parent="10" name="name">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="57" parent="10" name="value">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>''</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="58" parent="10" name="position">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="59" parent="10" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="60" parent="10" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="61" parent="10" name="isDeleted">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="62" parent="10" name="sqlite_autoindex_labels_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>labelId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="63" parent="10" name="IDX_labels_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="64" parent="10" name="IDX_labels_name_value">
 | 
			
		||||
      <ColNames>name
 | 
			
		||||
value</ColNames>
 | 
			
		||||
      <ColumnCollations>
 | 
			
		||||
</ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="65" parent="10">
 | 
			
		||||
      <ColNames>labelId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_labels_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="66" parent="11" name="noteImageId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="67" parent="11" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="68" parent="11" name="imageId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="69" parent="11" name="isDeleted">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="70" parent="11" name="dateModified">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="71" parent="11" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="72" parent="11" name="sqlite_autoindex_note_images_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteImageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="73" parent="11" name="IDX_note_images_noteId_imageId">
 | 
			
		||||
      <ColNames>noteId
 | 
			
		||||
imageId</ColNames>
 | 
			
		||||
      <ColumnCollations>
 | 
			
		||||
</ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="74" parent="11" name="IDX_note_images_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="75" parent="11" name="IDX_note_images_imageId">
 | 
			
		||||
      <ColNames>imageId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="76" parent="11">
 | 
			
		||||
      <ColNames>noteImageId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_note_images_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="77" parent="12" name="noteRevisionId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="78" parent="12" name="noteId">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="79" parent="12" name="title">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="80" parent="12" name="content">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="81" parent="12" name="isProtected">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="82" parent="12" name="dateModifiedFrom">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="83" parent="12" name="dateModifiedTo">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="84" parent="12" name="sqlite_autoindex_note_revisions_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteRevisionId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="85" parent="12" name="IDX_note_revisions_noteId">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="86" parent="12" name="IDX_note_revisions_dateModifiedFrom">
 | 
			
		||||
      <ColNames>dateModifiedFrom</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="87" parent="12" name="IDX_note_revisions_dateModifiedTo">
 | 
			
		||||
      <ColNames>dateModifiedTo</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="88" parent="12">
 | 
			
		||||
      <ColNames>noteRevisionId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_note_revisions_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="89" parent="13" name="noteId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="90" parent="13" name="title">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="91" parent="13" name="content">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="92" parent="13" name="isProtected">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="93" parent="13" name="isDeleted">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="94" parent="13" name="dateCreated">
 | 
			
		||||
      <Position>6</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="95" parent="13" name="dateModified">
 | 
			
		||||
      <Position>7</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="96" parent="13" name="type">
 | 
			
		||||
      <Position>8</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>'text'</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="97" parent="13" name="mime">
 | 
			
		||||
      <Position>9</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>'text/html'</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="98" parent="13" name="sqlite_autoindex_notes_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="99" parent="13" name="IDX_notes_isDeleted">
 | 
			
		||||
      <ColNames>isDeleted</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="100" parent="13">
 | 
			
		||||
      <ColNames>noteId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_notes_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="101" parent="14" name="name">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="102" parent="14" name="value">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="103" parent="14" name="dateModified">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="104" parent="14" name="isSynced">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>0</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="105" parent="14" name="sqlite_autoindex_options_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>name</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="106" parent="14">
 | 
			
		||||
      <ColNames>name</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_options_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="107" parent="15" name="branchId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="108" parent="15" name="notePath">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="109" parent="15" name="dateAccessed">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="110" parent="15" name="isDeleted">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>INT|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="111" parent="15" name="sqlite_autoindex_recent_notes_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="112" parent="15">
 | 
			
		||||
      <ColNames>branchId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_recent_notes_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="113" parent="16" name="sourceId">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="114" parent="16" name="dateCreated">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="115" parent="16" name="sqlite_autoindex_source_ids_1">
 | 
			
		||||
      <NameSurrogate>1</NameSurrogate>
 | 
			
		||||
      <ColNames>sourceId</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="116" parent="16">
 | 
			
		||||
      <ColNames>sourceId</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
      <UnderlyingIndexName>sqlite_autoindex_source_ids_1</UnderlyingIndexName>
 | 
			
		||||
    </key>
 | 
			
		||||
    <column id="117" parent="17" name="type">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="118" parent="17" name="name">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="119" parent="17" name="tbl_name">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="120" parent="17" name="rootpage">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>integer|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="121" parent="17" name="sql">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>text|0s</DataType>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="122" parent="18" name="name">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="123" parent="18" name="seq">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="124" parent="19" name="id">
 | 
			
		||||
      <Position>1</Position>
 | 
			
		||||
      <DataType>INTEGER|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <SequenceIdentity>1</SequenceIdentity>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="125" parent="19" name="entityName">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="126" parent="19" name="entityId">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="127" parent="19" name="sourceId">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="128" parent="19" name="syncDate">
 | 
			
		||||
      <Position>5</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
    </column>
 | 
			
		||||
    <index id="129" parent="19" name="IDX_sync_entityName_entityId">
 | 
			
		||||
      <ColNames>entityName
 | 
			
		||||
entityId</ColNames>
 | 
			
		||||
      <ColumnCollations>
 | 
			
		||||
</ColumnCollations>
 | 
			
		||||
      <Unique>1</Unique>
 | 
			
		||||
    </index>
 | 
			
		||||
    <index id="130" parent="19" name="IDX_sync_syncDate">
 | 
			
		||||
      <ColNames>syncDate</ColNames>
 | 
			
		||||
      <ColumnCollations></ColumnCollations>
 | 
			
		||||
    </index>
 | 
			
		||||
    <key id="131" parent="19">
 | 
			
		||||
      <ColNames>id</ColNames>
 | 
			
		||||
      <Primary>1</Primary>
 | 
			
		||||
    </key>
 | 
			
		||||
  </database-model>
 | 
			
		||||
</dataSource>
 | 
			
		||||
							
								
								
									
										2
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997/storage_v2/_src_/schema/main.uQUzAA.meta
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.idea/dataSources/a2c75661-f9e2-478f-a69f-6a9409e69997/storage_v2/_src_/schema/main.uQUzAA.meta
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
#n:main
 | 
			
		||||
!<md> [0, 0, null, null, -2147483648, -2147483648]
 | 
			
		||||
							
								
								
									
										10
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
<component name="InspectionProjectProfileManager">
 | 
			
		||||
  <profile version="1.0">
 | 
			
		||||
    <option name="myName" value="Project Default" />
 | 
			
		||||
    <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
 | 
			
		||||
      <option name="processCode" value="true" />
 | 
			
		||||
      <option name="processLiterals" value="true" />
 | 
			
		||||
      <option name="processComments" value="true" />
 | 
			
		||||
    </inspection_tool>
 | 
			
		||||
  </profile>
 | 
			
		||||
</component>
 | 
			
		||||
							
								
								
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/jsLinters/jslint.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="JSLintConfiguration">
 | 
			
		||||
    <option devel="true" />
 | 
			
		||||
    <option es6="true" />
 | 
			
		||||
    <option maxerr="50" />
 | 
			
		||||
    <option node="true" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										9
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="JavaScriptSettings">
 | 
			
		||||
    <option name="languageLevel" value="ES6" />
 | 
			
		||||
  </component>
 | 
			
		||||
  <component name="ProjectRootManager">
 | 
			
		||||
    <output url="file://$PROJECT_DIR$/out" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="ProjectModuleManager">
 | 
			
		||||
    <modules>
 | 
			
		||||
      <module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" />
 | 
			
		||||
    </modules>
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
							
								
								
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/vcs.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project version="4">
 | 
			
		||||
  <component name="VcsDirectoryMappings">
 | 
			
		||||
    <mapping directory="" vcs="Git" />
 | 
			
		||||
  </component>
 | 
			
		||||
</project>
 | 
			
		||||
@@ -1,4 +1,6 @@
 | 
			
		||||
# Trilium Notes
 | 
			
		||||
 | 
			
		||||
[](https://gitter.im/trilium-notes/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 | 
			
		||||
Trilium Notes is a hierarchical note taking application. Picture tells a thousand words:
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
@@ -10,7 +12,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan
 | 
			
		||||
* WYSIWYG (What You See Is What You Get) editing
 | 
			
		||||
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
			
		||||
* Seamless note versioning
 | 
			
		||||
* Note attributes can be used to tag/label notes as an alternative note organization and querying
 | 
			
		||||
* Note labels can be used to tag/label notes as an alternative note organization and querying
 | 
			
		||||
* Can be deployed as web application and / or desktop application with offline access (electron based)
 | 
			
		||||
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
 | 
			
		||||
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
			
		||||
@@ -35,11 +37,12 @@ List of documentation pages:
 | 
			
		||||
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
 | 
			
		||||
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
			
		||||
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
 | 
			
		||||
* [Attributes](https://github.com/zadam/trilium/wiki/Attributes)
 | 
			
		||||
* [Labels](https://github.com/zadam/trilium/wiki/Labels)
 | 
			
		||||
* [Links](https://github.com/zadam/trilium/wiki/Links)
 | 
			
		||||
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
 | 
			
		||||
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
			
		||||
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization)
 | 
			
		||||
* [Document](https://github.com/zadam/trilium/wiki/Document)
 | 
			
		||||
* [Theming](https://github.com/zadam/trilium/wiki/Theming)
 | 
			
		||||
* [Keyboard shortcuts](https://github.com/zadam/trilium/wiki/Keyboard-shortcuts)
 | 
			
		||||
* [Troubleshooting](https://github.com/zadam/trilium/wiki/Troubleshooting)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,9 @@ jq '.version = "'$VERSION'"' package.json|sponge package.json
 | 
			
		||||
 | 
			
		||||
git add package.json
 | 
			
		||||
 | 
			
		||||
echo 'module.exports = { build_date:"'`date --iso-8601=seconds`'", build_revision: "'`git log -1 --format="%H"`'" };' > services/build.js
 | 
			
		||||
echo 'module.exports = { buildDate:"'`date --iso-8601=seconds`'", buildRevision: "'`git log -1 --format="%H"`'" };' > src/services/build.js
 | 
			
		||||
 | 
			
		||||
git add services/build.js
 | 
			
		||||
git add src/services/build.js
 | 
			
		||||
 | 
			
		||||
TAG=v$VERSION
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,7 @@
 | 
			
		||||
[General]
 | 
			
		||||
# Instance name can be used to distinguish between different instances
 | 
			
		||||
instanceName=
 | 
			
		||||
 | 
			
		||||
[Network]
 | 
			
		||||
port=8080
 | 
			
		||||
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								db/main_branches.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								db/main_branches.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z');
 | 
			
		||||
@@ -1,52 +0,0 @@
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('wLTa2l3lYi83', 'HJusZTbBU494', '3RkyK9LI18dO', 2, null, 1, 0, '2017-12-23T01:20:50.709Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('yMhwsE7uvEij', '3oldoiMUPOlr', 'HJusZTbBU494', 1, null, 1, 0, '2017-12-23T01:20:55.775Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('EjQTcVVHFmmZ', 'MG0wntwILQW6', '3oldoiMUPOlr', 1, null, 1, 0, '2017-12-23T01:21:10.517Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('jvhKcwz4pYTr', 'ZC78NlmdXeC6', 'WdWZFuWNVDZk', 0, null, 1, 0, '2017-12-23T04:06:21.579Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('CarTrwkGVcPz', 'NncfGH8dyNjJ', 'WdWZFuWNVDZk', 1, null, 0, 0, '2017-12-23T04:06:24.012Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6M7qPlr7at6N', 'eouCLkjbruai', 'NncfGH8dyNjJ', 0, null, 0, 0, '2017-12-23T01:23:28.291Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('tQgognnAH9WI', 'C44aq4mkaX67', 'NncfGH8dyNjJ', 1, null, 0, 0, '2017-12-23T01:23:31.879Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xyAi7MmgvAgR', 'C44aq4mkaX67', 'ZC78NlmdXeC6', 1, null, 0, 0, '2017-12-23T01:23:47.756Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('xQ3fjRp9yaPq', 'I6Cw88AirBBl', 'C44aq4mkaX67', 0, null, 0, 0, '2017-12-23T01:24:04.681Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('2GOsNT5LsvTP', 'mcEwFMSjhlvL', 'C44aq4mkaX67', 1, null, 0, 0, '2017-12-23T01:29:35.974Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RxUiraiR655R', 'CF2lUIJAr6Ey', 'NncfGH8dyNjJ', 2, null, 0, 0, '2017-12-23T01:34:37.658Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('mZuSrZ18Zmv0', 'xkXwueRoDNeN', 'MG0wntwILQW6', 0, null, 0, 0, '2017-12-23T01:35:40.306Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('hbcWTnEnXPwF', 'eXHZAKsMYgur', '1Heh2acXfPNt', 3, null, 1, 0, '2017-12-23T03:32:42.868Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('8a3aNxjG0nu7', '2WU27ekfy07E', 'eXHZAKsMYgur', 0, null, 0, 0, '2017-12-23T03:32:49.379Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('4Tu6vaPdCxCM', 'TjWEndYCCg7g', 'eXHZAKsMYgur', 1, null, 0, 0, '2017-12-23T03:33:23.584Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('lBPOmhP12egP', '8nRNDJGyGs2Z', 'TjWEndYCCg7g', 0, null, 0, 0, '2017-12-23T03:33:37.327Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('C5ipVqeDWySp', '9zSwD89vgzNO', '8nRNDJGyGs2Z', 0, null, 0, 0, '2017-12-23T03:37:04.912Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uSitzbGcSATJ', 'u5t1EvWa3CMO', 'TjWEndYCCg7g', 1, null, 0, 0, '2017-12-23T03:39:21.918Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GZ6aRI8rdSJt', '8nRNDJGyGs2Z', 'MG0wntwILQW6', 1, '', 0, 0, '2017-12-23T03:42:28.310Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('HsN4600rQoL9', 'Iha4YwchR413', '3oldoiMUPOlr', 0, null, 1, 0, '2017-12-23T03:44:30.945Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('uipfvAfwWRgx', '6ZuXjCSWgjB4', 'HJusZTbBU494', 0, null, 0, 0, '2017-12-23T03:44:54.096Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nMRpPWWH8WRk', 'GpGnjmcAPeWG', '6ZuXjCSWgjB4', 0, null, 1, 0, '2017-12-23T03:44:57.036Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4wt27WNjepw', '21K84UqGhqlt', 'GpGnjmcAPeWG', 0, null, 0, 0, '2017-12-23T03:45:10.933Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('0fpnraUGs9Kl', 'rz5t0r9Qr2WC', 'HJusZTbBU494', 2, null, 1, 0, '2017-12-23T03:45:20.914Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('d8L8zYlLTbym', 'R6pheWjdwmNU', 'rz5t0r9Qr2WC', 0, null, 1, 0, '2017-12-23T03:45:28.002Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T4USGzfllu5t', '5v5Dx6LMHXIO', 'Iha4YwchR413', 0, null, 0, 0, '2017-12-23T03:45:44.184Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('c4JgFNIobvQW', 'MLQjmREtcnJ3', 'R6pheWjdwmNU', 0, null, 0, 0, '2017-12-23T03:47:48.208Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('nfWjptAU2ZDg', 'pTTjrxgnvURB', 'R6pheWjdwmNU', 1, null, 0, 0, '2017-12-23T03:47:55.932Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('T2ToYBfyPy0g', 'cFK9sGYZaMWs', 'rz5t0r9Qr2WC', 1, null, 0, 0, '2017-12-23T03:49:32.210Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('NG4gbKOnsM3v', '21K84UqGhqlt', 'MLQjmREtcnJ3', 0, '28. 11. 2017', 0, 0, '2017-12-23T03:53:38.110Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Fstg4tkccO4N', '5v5Dx6LMHXIO', 'MLQjmREtcnJ3', 1, '21. 12. 2017', 0, 0, '2017-12-23T03:53:49.737Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('MN8B7qXDUViO', 'xkXwueRoDNeN', 'MLQjmREtcnJ3', 2, '22. 12. 2017', 0, 0, '2017-12-23T03:53:57.486Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('gSRkHpB7Bu3D', 'pOFVzbXLmzhX', 'R6pheWjdwmNU', 2, null, 0, 0, '2017-12-23T03:54:46.138Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('6brdjeWDOB6w', '0xtvjqrcGiRB', 'ZC78NlmdXeC6', 0, null, 0, 0, '2017-12-23T04:02:06.650Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('AqKUM2zUVFUF', 'Zl69uXBSen0w', 'ZC78NlmdXeC6', 2, null, 1, 0, '2017-12-23T04:02:16.685Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('Ez7NN2WVzRc4', '62BKAQMVP2KW', 'Zl69uXBSen0w', 1, null, 0, 0, '2017-12-23T04:02:39.164Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('t3vVElqMIQVa', 'h4OfLEAYspud', 'WdWZFuWNVDZk', 2, null, 1, 0, '2017-12-23T04:06:25.769Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('O983DHtLpgmr', '1hASbLRDL7oo', 'h4OfLEAYspud', 0, null, 0, 0, '2017-12-23T16:42:26.347Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('RsvL795Mk1bp', '1hASbLRDL7oo', 'GpGnjmcAPeWG', 1, '', 0, 0, '2017-12-23T04:04:56.830Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('79e4hrHLFmx6', 'jyqG9GucsMdn', 'Iha4YwchR413', 1, null, 0, 0, '2017-12-23T04:05:16.439Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('oWO8rctUjf7d', 'WdWZFuWNVDZk', '1Heh2acXfPNt', 5, null, 1, 0, '2017-12-23T04:06:16.179Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('GOxcrZrxalFN', 'yK4SBJfwD3tY', '1Heh2acXfPNt', 8, null, 1, 0, '2017-12-23T04:06:32.833Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bSPmEvjLzQKU', 'r4BnsmSQeVr1', 'yK4SBJfwD3tY', 0, null, 0, 0, '2017-12-23T04:06:37.427Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bMtxCD6cwNR9', 'QbL3pTvhgzM8', 'yK4SBJfwD3tY', 2, null, 0, 0, '2017-12-23T04:06:43.841Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('o4ycR7xIi4oI', 'moMbTKwN15Ps', 'yK4SBJfwD3tY', 3, null, 1, 0, '2017-12-23T04:06:49.331Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('abTEhnOsAsSg', 'PEGQGg0In3Ar', 'GpGnjmcAPeWG', 2, null, 0, 0, '2017-12-23T16:44:35.900Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('bryQseMhyzaI', 'IlULcDiOTI4K', '1Heh2acXfPNt', 0, null, 0, 0, '2017-12-23T18:04:26.439Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('ccslPJf3wQV3', 'vBv6ovBupfTj', 'IlULcDiOTI4K', 0, null, 0, 0, '2017-12-23T18:04:50.904Z');
 | 
			
		||||
INSERT INTO note_tree (noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('5Dt9YCMn59sY', 'mw4f2xB4J5fV', 'IlULcDiOTI4K', 1, null, 0, 0, '2017-12-23T18:05:24.868Z');
 | 
			
		||||
							
								
								
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';
 | 
			
		||||
							
								
								
									
										38
									
								
								db/migrations/0079__rename_note_tree.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								db/migrations/0079__rename_note_tree.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
CREATE TABLE "branches" (
 | 
			
		||||
  `branchId`	TEXT NOT NULL,
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
  `parentNoteId`	TEXT NOT NULL,
 | 
			
		||||
  `notePosition`	INTEGER NOT NULL,
 | 
			
		||||
  `prefix`	TEXT,
 | 
			
		||||
  `isExpanded`	BOOLEAN,
 | 
			
		||||
  `isDeleted`	INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL,
 | 
			
		||||
  PRIMARY KEY(`branchId`)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified)
 | 
			
		||||
    SELECT noteTreeId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified FROM note_tree;
 | 
			
		||||
 | 
			
		||||
DROP TABLE note_tree;
 | 
			
		||||
 | 
			
		||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
 | 
			
		||||
  `noteId`,
 | 
			
		||||
  `parentNoteId`
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `recent_notes_mig` (
 | 
			
		||||
  `branchId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  `dateAccessed` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
INSERT INTO recent_notes_mig (branchId, notePath, dateAccessed, isDeleted)
 | 
			
		||||
    SELECT noteTreeId, notePath, dateAccessed, isDeleted FROM recent_notes;
 | 
			
		||||
 | 
			
		||||
DROP TABLE recent_notes;
 | 
			
		||||
ALTER TABLE recent_notes_mig RENAME TO recent_notes;
 | 
			
		||||
							
								
								
									
										22
									
								
								db/migrations/0080__rename_attributes_to_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								db/migrations/0080__rename_attributes_to_labels.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
create table labels
 | 
			
		||||
(
 | 
			
		||||
  labelId  TEXT not null primary key,
 | 
			
		||||
  noteId       TEXT not null,
 | 
			
		||||
  name         TEXT not null,
 | 
			
		||||
  value        TEXT default '' not null,
 | 
			
		||||
  position     INT  default 0 not null,
 | 
			
		||||
  dateCreated  TEXT not null,
 | 
			
		||||
  dateModified TEXT not null,
 | 
			
		||||
  isDeleted    INT  not null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
create index IDX_labels_name_value
 | 
			
		||||
  on labels (name, value);
 | 
			
		||||
 | 
			
		||||
create index IDX_labels_noteId
 | 
			
		||||
  on labels (noteId);
 | 
			
		||||
 | 
			
		||||
INSERT INTO labels (labelId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted)
 | 
			
		||||
  SELECT attributeId, noteId, name, "value", "position", dateCreated, dateModified, isDeleted FROM attributes;
 | 
			
		||||
 | 
			
		||||
DROP TABLE attributes;
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
UPDATE options SET name = 'note_revision_snapshot_time_interval' WHERE name = 'history_snapshot_time_interval';
 | 
			
		||||
							
								
								
									
										14
									
								
								db/migrations/0082__camelCase_options.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								db/migrations/0082__camelCase_options.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
UPDATE "options" SET "name" = 'passwordVerificationHash' WHERE "name" = 'password_verification_hash';
 | 
			
		||||
UPDATE "options" SET "name" = 'dbVersion' WHERE "name" = 'db_version';
 | 
			
		||||
UPDATE "options" SET "name" = 'passwordDerivedKeySalt' WHERE "name" = 'password_derived_key_salt';
 | 
			
		||||
UPDATE "options" SET "name" = 'documentId' WHERE "name" = 'document_id';
 | 
			
		||||
UPDATE "options" SET "name" = 'lastSyncedPull' WHERE "name" = 'last_synced_pull';
 | 
			
		||||
UPDATE "options" SET "name" = 'startNotePath' WHERE "name" = 'start_note_path';
 | 
			
		||||
UPDATE "options" SET "name" = 'lastSyncedPush' WHERE "name" = 'last_synced_push';
 | 
			
		||||
UPDATE "options" SET "name" = 'documentSecret' WHERE "name" = 'document_secret';
 | 
			
		||||
UPDATE "options" SET "name" = 'lastBackupDate' WHERE "name" = 'last_backup_date';
 | 
			
		||||
UPDATE "options" SET "name" = 'noteRevisionSnapshotTimeInterval' WHERE "name" = 'note_revision_snapshot_time_interval';
 | 
			
		||||
UPDATE "options" SET "name" = 'protectedSessionTimeout' WHERE "name" = 'protected_session_timeout';
 | 
			
		||||
UPDATE "options" SET "name" = 'encryptedDataKey' WHERE "name" = 'encrypted_data_key';
 | 
			
		||||
UPDATE "options" SET "name" = 'encryptedDataKeyIv' WHERE "name" = 'encrypted_data_key_iv';
 | 
			
		||||
UPDATE "options" SET "name" = 'passwordVerificationSalt' WHERE "name" = 'password_verification_salt';
 | 
			
		||||
							
								
								
									
										7
									
								
								db/migrations/0083__camelCase_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrations/0083__camelCase_labels.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
UPDATE labels SET name = 'disableVersioning' WHERE name = 'disable_versioning';
 | 
			
		||||
UPDATE labels SET name = 'calendarRoot' WHERE name = 'calendar_root';
 | 
			
		||||
UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete';
 | 
			
		||||
UPDATE labels SET name = 'excludeFromExport' WHERE name = 'exclude_from_export';
 | 
			
		||||
UPDATE labels SET name = 'manualTransactionHandling' WHERE name = 'manual_transaction_handling';
 | 
			
		||||
UPDATE labels SET name = 'disableInclusion' WHERE name = 'disable_inclusion';
 | 
			
		||||
UPDATE labels SET name = 'appCss' WHERE name = 'app_css';
 | 
			
		||||
							
								
								
									
										4
									
								
								db/migrations/0084__camelCase_reddit_ids.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								db/migrations/0084__camelCase_reddit_ids.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
UPDATE labels SET name = 'redditId' WHERE name = 'reddit_id';
 | 
			
		||||
UPDATE labels SET name = 'redditKind' WHERE name = 'reddit_kind';
 | 
			
		||||
UPDATE labels SET name = 'redditCreatedUtc' WHERE name = 'reddit_created_utc';
 | 
			
		||||
UPDATE labels SET name = 'redditDateNote' WHERE name = 'reddit_date_note';
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0085__camelCase_run_values.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								db/migrations/0085__camelCase_run_values.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
UPDATE labels SET value = 'frontendStartup' WHERE value = 'frontend_startup';
 | 
			
		||||
UPDATE labels SET value = 'backendStartup' WHERE value = 'backend_startup';
 | 
			
		||||
							
								
								
									
										7
									
								
								db/migrations/0086__camelCase_custom_labels.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrations/0086__camelCase_custom_labels.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
UPDATE labels SET name = 'dateData' WHERE name = 'date_data';
 | 
			
		||||
UPDATE labels SET name = 'dateNote' WHERE name = 'date_note';
 | 
			
		||||
UPDATE labels SET name = 'fileSize' WHERE name = 'file_size';
 | 
			
		||||
UPDATE labels SET name = 'hideInAutocomplete' WHERE name = 'hide_in_autocomplete';
 | 
			
		||||
UPDATE labels SET name = 'monthNote' WHERE name = 'month_note';
 | 
			
		||||
UPDATE labels SET name = 'originalFileName' WHERE name = 'original_file_name';
 | 
			
		||||
UPDATE labels SET name = 'yearNote' WHERE name = 'year_note';
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrations/0087__add_type_mime_to_note_revision.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrations/0087__add_type_mime_to_note_revision.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
ALTER TABLE note_revisions ADD type TEXT DEFAULT '' NOT NULL;
 | 
			
		||||
ALTER TABLE note_revisions ADD mime TEXT DEFAULT '' NOT NULL;
 | 
			
		||||
 | 
			
		||||
UPDATE note_revisions SET type = (SELECT type FROM notes WHERE notes.noteId = note_revisions.noteId);
 | 
			
		||||
UPDATE note_revisions SET mime = (SELECT mime FROM notes WHERE notes.noteId = note_revisions.noteId);
 | 
			
		||||
							
								
								
									
										113
									
								
								db/schema.sql
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								db/schema.sql
									
									
									
									
									
								
							@@ -9,6 +9,13 @@ CREATE TABLE IF NOT EXISTS "sync" (
 | 
			
		||||
  `entityId`	TEXT NOT NULL,
 | 
			
		||||
  `sourceId` TEXT NOT NULL,
 | 
			
		||||
  `syncDate`	TEXT NOT NULL);
 | 
			
		||||
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
 | 
			
		||||
  `entityName`,
 | 
			
		||||
  `entityId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
 | 
			
		||||
  `syncDate`
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "source_ids" (
 | 
			
		||||
  `sourceId`	TEXT NOT NULL,
 | 
			
		||||
  `dateCreated`	TEXT NOT NULL,
 | 
			
		||||
@@ -26,6 +33,9 @@ CREATE TABLE IF NOT EXISTS "notes" (
 | 
			
		||||
  mime TEXT NOT NULL DEFAULT 'text/html',
 | 
			
		||||
  PRIMARY KEY(`noteId`)
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
 | 
			
		||||
  `isDeleted`
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "event_log" (
 | 
			
		||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
			
		||||
  `noteId`	TEXT,
 | 
			
		||||
@@ -33,17 +43,6 @@ CREATE TABLE IF NOT EXISTS "event_log" (
 | 
			
		||||
  `dateAdded`	TEXT NOT NULL,
 | 
			
		||||
  FOREIGN KEY(noteId) REFERENCES notes(noteId)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "note_tree" (
 | 
			
		||||
  `noteTreeId`	TEXT NOT NULL,
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
  `parentNoteId`	TEXT NOT NULL,
 | 
			
		||||
  `notePosition`	INTEGER NOT NULL,
 | 
			
		||||
  `prefix`	TEXT,
 | 
			
		||||
  `isExpanded`	BOOLEAN,
 | 
			
		||||
  `isDeleted`	INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL,
 | 
			
		||||
  PRIMARY KEY(`noteTreeId`)
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "note_revisions" (
 | 
			
		||||
  `noteRevisionId`	TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
@@ -53,11 +52,14 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
 | 
			
		||||
  `dateModifiedFrom` TEXT NOT NULL,
 | 
			
		||||
  `dateModifiedTo` TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
 | 
			
		||||
  `noteTreeId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  `dateAccessed` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
 | 
			
		||||
  `dateModifiedFrom`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
 | 
			
		||||
  `dateModifiedTo`
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "images"
 | 
			
		||||
(
 | 
			
		||||
@@ -79,49 +81,9 @@ CREATE TABLE note_images
 | 
			
		||||
  dateModified TEXT NOT NULL,
 | 
			
		||||
  dateCreated TEXT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "attributes"
 | 
			
		||||
(
 | 
			
		||||
  attributeId TEXT PRIMARY KEY NOT NULL,
 | 
			
		||||
  noteId TEXT NOT NULL,
 | 
			
		||||
  name TEXT NOT NULL,
 | 
			
		||||
  value TEXT,
 | 
			
		||||
  position INT NOT NULL DEFAULT 0,
 | 
			
		||||
  dateCreated TEXT NOT NULL,
 | 
			
		||||
  dateModified TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT NOT NULL
 | 
			
		||||
);
 | 
			
		||||
CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
 | 
			
		||||
  `entityName`,
 | 
			
		||||
  `entityId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_sync_syncDate` ON `sync` (
 | 
			
		||||
  `syncDate`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
 | 
			
		||||
  `isDeleted`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` (
 | 
			
		||||
  `noteId`,
 | 
			
		||||
  `parentNoteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
 | 
			
		||||
  `dateModifiedFrom`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
 | 
			
		||||
  `dateModifiedTo`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
 | 
			
		||||
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
 | 
			
		||||
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
 | 
			
		||||
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
 | 
			
		||||
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "api_tokens"
 | 
			
		||||
(
 | 
			
		||||
  apiTokenId TEXT PRIMARY KEY NOT NULL,
 | 
			
		||||
@@ -129,3 +91,42 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
 | 
			
		||||
  dateCreated TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT NOT NULL DEFAULT 0
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "branches" (
 | 
			
		||||
  `branchId`	TEXT NOT NULL,
 | 
			
		||||
  `noteId`	TEXT NOT NULL,
 | 
			
		||||
  `parentNoteId`	TEXT NOT NULL,
 | 
			
		||||
  `notePosition`	INTEGER NOT NULL,
 | 
			
		||||
  `prefix`	TEXT,
 | 
			
		||||
  `isExpanded`	BOOLEAN,
 | 
			
		||||
  `isDeleted`	INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
  `dateModified`	TEXT NOT NULL,
 | 
			
		||||
  PRIMARY KEY(`branchId`)
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
 | 
			
		||||
  `noteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
 | 
			
		||||
  `noteId`,
 | 
			
		||||
  `parentNoteId`
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
 | 
			
		||||
  `branchId` TEXT NOT NULL PRIMARY KEY,
 | 
			
		||||
  `notePath` TEXT NOT NULL,
 | 
			
		||||
  `dateAccessed` TEXT NOT NULL,
 | 
			
		||||
  isDeleted INT
 | 
			
		||||
);
 | 
			
		||||
CREATE TABLE labels
 | 
			
		||||
(
 | 
			
		||||
  labelId  TEXT not null primary key,
 | 
			
		||||
  noteId       TEXT not null,
 | 
			
		||||
  name         TEXT not null,
 | 
			
		||||
  value        TEXT default '' not null,
 | 
			
		||||
  position     INT  default 0 not null,
 | 
			
		||||
  dateCreated  TEXT not null,
 | 
			
		||||
  dateModified TEXT not null,
 | 
			
		||||
  isDeleted    INT  not null
 | 
			
		||||
);
 | 
			
		||||
CREATE INDEX IDX_labels_name_value
 | 
			
		||||
  on labels (name, value);
 | 
			
		||||
CREATE INDEX IDX_labels_noteId
 | 
			
		||||
  on labels (noteId);
 | 
			
		||||
 
 | 
			
		||||
@@ -73,15 +73,15 @@ app.on('ready', () => {
 | 
			
		||||
    mainWindow = createMainWindow();
 | 
			
		||||
 | 
			
		||||
    const result = globalShortcut.register('CommandOrControl+Alt+P', async () => {
 | 
			
		||||
        const date_notes = require('./src/services/date_notes');
 | 
			
		||||
        const utils = require('./src/services/utils');
 | 
			
		||||
        const dateNoteService = require('./src/services/date_notes');
 | 
			
		||||
        const dateUtils = require('./src/services/date_utils');
 | 
			
		||||
 | 
			
		||||
        const parentNoteId = await date_notes.getDateNoteId(utils.nowDate());
 | 
			
		||||
        const parentNote = await dateNoteService.getDateNote(dateUtils.nowDate());
 | 
			
		||||
 | 
			
		||||
        // window may be hidden / not in focus
 | 
			
		||||
        mainWindow.focus();
 | 
			
		||||
 | 
			
		||||
        mainWindow.webContents.send('create-day-sub-note', parentNoteId);
 | 
			
		||||
        mainWindow.webContents.send('create-day-sub-note', parentNote.noteId);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!result) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1475
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1475
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "description": "Trilium Notes",
 | 
			
		||||
  "version": "0.7.0-beta",
 | 
			
		||||
  "version": "0.11.0-beta",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "electron.js",
 | 
			
		||||
  "repository": {
 | 
			
		||||
@@ -12,8 +12,8 @@
 | 
			
		||||
    "start": "node ./bin/www",
 | 
			
		||||
    "test-electron": "xo",
 | 
			
		||||
    "rebuild-electron": "electron-rebuild",
 | 
			
		||||
    "start-electron": "electron .",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --all",
 | 
			
		||||
    "start-electron": "electron . --disable-gpu",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
 | 
			
		||||
    "start-forge": "electron-forge start",
 | 
			
		||||
    "package-forge": "electron-forge package",
 | 
			
		||||
    "make-forge": "electron-forge make",
 | 
			
		||||
@@ -23,49 +23,51 @@
 | 
			
		||||
    "async-mutex": "^0.1.3",
 | 
			
		||||
    "axios": "^0.17.1",
 | 
			
		||||
    "body-parser": "~1.18.2",
 | 
			
		||||
    "cls-hooked": "^4.2.2",
 | 
			
		||||
    "cookie-parser": "~1.4.3",
 | 
			
		||||
    "debug": "~3.1.0",
 | 
			
		||||
    "devtron": "^1.4.0",
 | 
			
		||||
    "ejs": "~2.5.7",
 | 
			
		||||
    "electron": "^1.8.2",
 | 
			
		||||
    "electron": "^2.0.0-beta.5",
 | 
			
		||||
    "electron-debug": "^1.5.0",
 | 
			
		||||
    "electron-dl": "^1.11.0",
 | 
			
		||||
    "electron-in-page-search": "^1.2.4",
 | 
			
		||||
    "express": "~4.16.2",
 | 
			
		||||
    "express-promise-wrap": "^0.2.2",
 | 
			
		||||
    "electron-rebuild": "^1.7.3",
 | 
			
		||||
    "express": "~4.16.3",
 | 
			
		||||
    "express-session": "^1.15.6",
 | 
			
		||||
    "fs-extra": "^4.0.2",
 | 
			
		||||
    "helmet": "^3.9.0",
 | 
			
		||||
    "fs-extra": "^4.0.3",
 | 
			
		||||
    "helmet": "^3.12.0",
 | 
			
		||||
    "html": "^1.0.0",
 | 
			
		||||
    "image-type": "^3.0.0",
 | 
			
		||||
    "imagemin": "^5.3.1",
 | 
			
		||||
    "imagemin-giflossy": "^5.1.10",
 | 
			
		||||
    "imagemin-mozjpeg": "^7.0.0",
 | 
			
		||||
    "imagemin-pngquant": "^5.0.1",
 | 
			
		||||
    "ini": "^1.3.4",
 | 
			
		||||
    "imagemin-pngquant": "^5.1.0",
 | 
			
		||||
    "ini": "^1.3.5",
 | 
			
		||||
    "jimp": "^0.2.28",
 | 
			
		||||
    "moment": "^2.20.1",
 | 
			
		||||
    "moment": "^2.21.0",
 | 
			
		||||
    "multer": "^1.3.0",
 | 
			
		||||
    "open": "0.0.5",
 | 
			
		||||
    "rand-token": "^0.4.0",
 | 
			
		||||
    "request": "^2.83.0",
 | 
			
		||||
    "request": "^2.85.0",
 | 
			
		||||
    "request-promise": "^4.2.2",
 | 
			
		||||
    "rimraf": "^2.6.2",
 | 
			
		||||
    "sanitize-filename": "^1.6.1",
 | 
			
		||||
    "scrypt": "^6.0.3",
 | 
			
		||||
    "serve-favicon": "~2.4.5",
 | 
			
		||||
    "session-file-store": "^1.1.2",
 | 
			
		||||
    "simple-node-logger": "^0.93.30",
 | 
			
		||||
    "sqlite": "^2.9.0",
 | 
			
		||||
    "session-file-store": "^1.2.0",
 | 
			
		||||
    "simple-node-logger": "^0.93.37",
 | 
			
		||||
    "sqlite": "^2.9.1",
 | 
			
		||||
    "tar-stream": "^1.5.5",
 | 
			
		||||
    "unescape": "^1.0.1",
 | 
			
		||||
    "ws": "^3.3.2"
 | 
			
		||||
    "ws": "^3.3.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "electron-compile": "^6.4.2",
 | 
			
		||||
    "electron-packager": "^11.0.1",
 | 
			
		||||
    "electron-prebuilt-compile": "1.8.2",
 | 
			
		||||
    "electron-rebuild": "^1.7.3",
 | 
			
		||||
    "tape": "^4.8.0",
 | 
			
		||||
    "electron-packager": "^11.1.0",
 | 
			
		||||
    "electron-prebuilt-compile": "2.0.0-beta.5",
 | 
			
		||||
    "lorem-ipsum": "^1.0.4",
 | 
			
		||||
    "tape": "^4.9.0",
 | 
			
		||||
    "xo": "^0.18.0"
 | 
			
		||||
  },
 | 
			
		||||
  "config": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/app.js
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/app.js
									
									
									
									
									
								
							@@ -9,6 +9,8 @@ const session = require('express-session');
 | 
			
		||||
const FileStore = require('session-file-store')(session);
 | 
			
		||||
const os = require('os');
 | 
			
		||||
const sessionSecret = require('./services/session_secret');
 | 
			
		||||
const cls = require('./services/cls');
 | 
			
		||||
require('./entities/entity_constructor');
 | 
			
		||||
 | 
			
		||||
const app = express();
 | 
			
		||||
 | 
			
		||||
@@ -23,6 +25,17 @@ app.use((req, res, next) => {
 | 
			
		||||
    next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.use((req, res, next) => {
 | 
			
		||||
    cls.namespace.bindEmitter(req);
 | 
			
		||||
    cls.namespace.bindEmitter(res);
 | 
			
		||||
 | 
			
		||||
    cls.init(() => {
 | 
			
		||||
        cls.namespace.set("Hi");
 | 
			
		||||
 | 
			
		||||
        next();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.use(bodyParser.json({limit: '50mb'}));
 | 
			
		||||
app.use(bodyParser.urlencoded({extended: false}));
 | 
			
		||||
app.use(cookieParser());
 | 
			
		||||
@@ -73,7 +86,7 @@ require('./services/backup');
 | 
			
		||||
// trigger consistency checks timer
 | 
			
		||||
require('./services/consistency_checks');
 | 
			
		||||
 | 
			
		||||
require('./plugins/reddit');
 | 
			
		||||
require('./services/scheduler');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    app,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								src/entities/api_token.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/entities/api_token.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class ApiToken extends Entity {
 | 
			
		||||
    static get tableName() { return "api_tokens"; }
 | 
			
		||||
    static get primaryKeyName() { return "apiTokenId"; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = ApiToken;
 | 
			
		||||
@@ -1,14 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
 | 
			
		||||
class Attribute extends Entity {
 | 
			
		||||
    static get tableName() { return "attributes"; }
 | 
			
		||||
    static get primaryKeyName() { return "attributeId"; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Attribute;
 | 
			
		||||
							
								
								
									
										32
									
								
								src/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
const sql = require('../services/sql');
 | 
			
		||||
 | 
			
		||||
class Branch extends Entity {
 | 
			
		||||
    static get tableName() { return "branches"; }
 | 
			
		||||
    static get primaryKeyName() { return "branchId"; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (this.notePosition === undefined) {
 | 
			
		||||
            const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
 | 
			
		||||
            this.notePosition = maxNotePos === null ? 0 : maxNotePos + 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Branch;
 | 
			
		||||
@@ -1,17 +1,26 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
class Entity {
 | 
			
		||||
    constructor(repository, row) {
 | 
			
		||||
        utils.assertArguments(repository, row);
 | 
			
		||||
 | 
			
		||||
        this.repository = repository;
 | 
			
		||||
 | 
			
		||||
    constructor(row = {}) {
 | 
			
		||||
        for (const key in row) {
 | 
			
		||||
            this[key] = row[key];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        if (!this[this.constructor.primaryKeyName]) {
 | 
			
		||||
            this[this.constructor.primaryKeyName] = utils.newEntityId();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async save() {
 | 
			
		||||
        await repository.updateEntity(this);
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Entity;
 | 
			
		||||
							
								
								
									
										49
									
								
								src/entities/entity_constructor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/entities/entity_constructor.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
const Note = require('../entities/note');
 | 
			
		||||
const NoteRevision = require('../entities/note_revision');
 | 
			
		||||
const Image = require('../entities/image');
 | 
			
		||||
const NoteImage = require('../entities/note_image');
 | 
			
		||||
const Branch = require('../entities/branch');
 | 
			
		||||
const Label = require('../entities/label');
 | 
			
		||||
const RecentNote = require('../entities/recent_note');
 | 
			
		||||
const ApiToken = require('../entities/api_token');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
function createEntityFromRow(row) {
 | 
			
		||||
    let entity;
 | 
			
		||||
 | 
			
		||||
    if (row.labelId) {
 | 
			
		||||
        entity = new Label(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.noteRevisionId) {
 | 
			
		||||
        entity = new NoteRevision(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.noteImageId) {
 | 
			
		||||
        entity = new NoteImage(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.imageId) {
 | 
			
		||||
        entity = new Image(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.branchId && row.notePath) {
 | 
			
		||||
        entity = new RecentNote(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.apiTokenId) {
 | 
			
		||||
        entity = new ApiToken(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.branchId) {
 | 
			
		||||
        entity = new Branch(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.noteId) {
 | 
			
		||||
        entity = new Note(row);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return entity;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
repository.setEntityConstructor(createEntityFromRow);
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    createEntityFromRow
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										25
									
								
								src/entities/image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/entities/image.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class Image extends Entity {
 | 
			
		||||
    static get tableName() { return "images"; }
 | 
			
		||||
    static get primaryKeyName() { return "imageId"; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Image;
 | 
			
		||||
							
								
								
									
										40
									
								
								src/entities/label.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/entities/label.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
const sql = require('../services/sql');
 | 
			
		||||
 | 
			
		||||
class Label extends Entity {
 | 
			
		||||
    static get tableName() { return "labels"; }
 | 
			
		||||
    static get primaryKeyName() { return "labelId"; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (!this.value) {
 | 
			
		||||
            // null value isn't allowed
 | 
			
		||||
            this.value = "";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.position === undefined) {
 | 
			
		||||
            this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM labels WHERE noteId = ?`, [this.noteId]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Label;
 | 
			
		||||
@@ -2,16 +2,19 @@
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class Note extends Entity {
 | 
			
		||||
    static get tableName() { return "notes"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteId"; }
 | 
			
		||||
 | 
			
		||||
    constructor(repository, row) {
 | 
			
		||||
        super(repository, row);
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.decryptNote(this.dataKey, this);
 | 
			
		||||
        // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
 | 
			
		||||
        if (this.isProtected && this.noteId) {
 | 
			
		||||
            protected_session.decryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isJson()) {
 | 
			
		||||
@@ -19,36 +22,143 @@ class Note extends Entity {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setContent(content) {
 | 
			
		||||
        this.content = content;
 | 
			
		||||
 | 
			
		||||
        if (this.isJson()) {
 | 
			
		||||
            this.jsonContent = JSON.parse(this.content);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isJson() {
 | 
			
		||||
        return this.type === "code" && this.mime === "application/json";
 | 
			
		||||
        return this.mime === "application/json";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isJavaScript() {
 | 
			
		||||
        return this.type === "code" && this.mime === "application/javascript";
 | 
			
		||||
        return (this.type === "code" || this.type === "file")
 | 
			
		||||
            && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getAttributes() {
 | 
			
		||||
        return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
			
		||||
    isHtml() {
 | 
			
		||||
        return (this.type === "code" || this.type === "file") && this.mime === "text/html";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getAttribute(name) {
 | 
			
		||||
        return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
 | 
			
		||||
    getScriptEnv() {
 | 
			
		||||
        if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
 | 
			
		||||
            return "frontend";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.type === 'render') {
 | 
			
		||||
            return "frontend";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
 | 
			
		||||
            return "backend";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getLabels() {
 | 
			
		||||
        return await repository.getEntities("SELECT * FROM labels WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // WARNING: this doesn't take into account the possibility to have multi-valued labels!
 | 
			
		||||
    async getLabelMap() {
 | 
			
		||||
        const map = {};
 | 
			
		||||
 | 
			
		||||
        for (const label of await this.getLabels()) {
 | 
			
		||||
            map[label.name] = label.value;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async hasLabel(name) {
 | 
			
		||||
        const map = await this.getLabelMap();
 | 
			
		||||
 | 
			
		||||
        return map.hasOwnProperty(name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // WARNING: this doesn't take into account the possibility to have multi-valued labels!
 | 
			
		||||
    async getLabel(name) {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM labels WHERE noteId = ? AND name = ?", [this.noteId, name]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getRevisions() {
 | 
			
		||||
        return this.repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
        return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getTrees() {
 | 
			
		||||
        return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
 | 
			
		||||
    async getNoteImages() {
 | 
			
		||||
        return await repository.getEntities("SELECT * FROM note_images WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBranches() {
 | 
			
		||||
        return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNote(name) {
 | 
			
		||||
        return await repository.getEntity(`
 | 
			
		||||
          SELECT notes.* 
 | 
			
		||||
          FROM branches 
 | 
			
		||||
            JOIN notes USING(noteId) 
 | 
			
		||||
          WHERE notes.isDeleted = 0
 | 
			
		||||
                AND branches.isDeleted = 0
 | 
			
		||||
                AND branches.parentNoteId = ?
 | 
			
		||||
                AND notes.title = ?`, [this.noteId, name]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
        return await repository.getEntities(`
 | 
			
		||||
          SELECT notes.* 
 | 
			
		||||
          FROM branches 
 | 
			
		||||
            JOIN notes USING(noteId) 
 | 
			
		||||
          WHERE notes.isDeleted = 0
 | 
			
		||||
                AND branches.isDeleted = 0
 | 
			
		||||
                AND branches.parentNoteId = ?
 | 
			
		||||
          ORDER BY branches.notePosition`, [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildBranches() {
 | 
			
		||||
        return await repository.getEntities(`
 | 
			
		||||
          SELECT branches.* 
 | 
			
		||||
          FROM branches 
 | 
			
		||||
          WHERE branches.isDeleted = 0
 | 
			
		||||
                AND branches.parentNoteId = ?
 | 
			
		||||
          ORDER BY branches.notePosition`, [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
        return await repository.getEntities(`
 | 
			
		||||
          SELECT parent_notes.* 
 | 
			
		||||
          FROM 
 | 
			
		||||
            branches AS child_tree 
 | 
			
		||||
            JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId 
 | 
			
		||||
          WHERE child_tree.noteId = ?
 | 
			
		||||
                AND child_tree.isDeleted = 0
 | 
			
		||||
                AND parent_notes.isDeleted = 0`, [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (this.isJson()) {
 | 
			
		||||
            this.content = JSON.stringify(this.jsonContent, null, '\t');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNote(this.dataKey, this);
 | 
			
		||||
            protected_session.encryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								src/entities/note_image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/entities/note_image.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class NoteImage extends Entity {
 | 
			
		||||
    static get tableName() { return "note_images"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteImageId"; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getImage() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM images WHERE imageId = ?", [this.imageId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
            this.isDeleted = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.dateCreated) {
 | 
			
		||||
            this.dateCreated = dateUtils.nowDate();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = NoteImage;
 | 
			
		||||
@@ -1,13 +1,32 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
class NoteRevision extends Entity {
 | 
			
		||||
    static get tableName() { return "note_revisions"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteRevisionId"; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.decryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return this.repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
 | 
			
		||||
class NoteTree extends Entity {
 | 
			
		||||
    static get tableName() { return "note_tree"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteTreeId"; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getParentNote() {
 | 
			
		||||
        return this.repository.getEntity("SELECT * FROM note_tree WHERE isDeleted = 0 AND parentNoteId = ?", [this.parentNoteId]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = NoteTree;
 | 
			
		||||
							
								
								
									
										10
									
								
								src/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/entities/recent_note.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
 | 
			
		||||
class RecentNote extends Entity {
 | 
			
		||||
    static get tableName() { return "recent_notes"; }
 | 
			
		||||
    static get primaryKeyName() { return "branchId"; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = RecentNote;
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const sql = require('../services/sql');
 | 
			
		||||
const notes = require('../services/notes');
 | 
			
		||||
const axios = require('axios');
 | 
			
		||||
const log = require('../services/log');
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const unescape = require('unescape');
 | 
			
		||||
const attributes = require('../services/attributes');
 | 
			
		||||
const sync_mutex = require('../services/sync_mutex');
 | 
			
		||||
const config = require('../services/config');
 | 
			
		||||
const date_notes = require('../services/date_notes');
 | 
			
		||||
 | 
			
		||||
// "reddit" date note is subnote of date note which contains all reddit comments from that date
 | 
			
		||||
const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note';
 | 
			
		||||
 | 
			
		||||
async function createNote(parentNoteId, noteTitle, noteText) {
 | 
			
		||||
    return (await notes.createNewNote(parentNoteId, {
 | 
			
		||||
        title: noteTitle,
 | 
			
		||||
        content: noteText,
 | 
			
		||||
        target: 'into',
 | 
			
		||||
        isProtected: false
 | 
			
		||||
    })).noteId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function redditId(kind, id) {
 | 
			
		||||
    return kind + "_" + id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
 | 
			
		||||
    const dateStr = dateTimeStr.substr(0, 10);
 | 
			
		||||
 | 
			
		||||
    let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr);
 | 
			
		||||
 | 
			
		||||
    if (!redditDateNoteId) {
 | 
			
		||||
        const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId);
 | 
			
		||||
 | 
			
		||||
        redditDateNoteId = await createNote(dateNoteId, "Reddit");
 | 
			
		||||
 | 
			
		||||
        await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
 | 
			
		||||
        await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return redditDateNoteId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function importComments(rootNoteId, accountName, afterId = null) {
 | 
			
		||||
    let url = `https://www.reddit.com/user/${accountName}.json`;
 | 
			
		||||
 | 
			
		||||
    if (afterId) {
 | 
			
		||||
        url += "?after=" + afterId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const response = await axios.get(url);
 | 
			
		||||
    const listing = response.data;
 | 
			
		||||
 | 
			
		||||
    if (listing.kind !== 'Listing') {
 | 
			
		||||
        log.info(`Reddit: Unknown object kind ${listing.kind}`);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const children = listing.data.children;
 | 
			
		||||
 | 
			
		||||
    let importedComments = 0;
 | 
			
		||||
 | 
			
		||||
    for (const child of children) {
 | 
			
		||||
        const comment = child.data;
 | 
			
		||||
 | 
			
		||||
        let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id));
 | 
			
		||||
 | 
			
		||||
        if (commentNoteId) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000));
 | 
			
		||||
 | 
			
		||||
        const permaLink = 'https://reddit.com' + comment.permalink;
 | 
			
		||||
 | 
			
		||||
        const noteText =
 | 
			
		||||
`<p><a href="${permaLink}">${permaLink}</a></p>
 | 
			
		||||
<p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>, 
 | 
			
		||||
subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>, 
 | 
			
		||||
karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>`
 | 
			
		||||
            + unescape(comment.body_html);
 | 
			
		||||
 | 
			
		||||
        let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId);
 | 
			
		||||
 | 
			
		||||
        await sql.doInTransaction(async () => {
 | 
			
		||||
            commentNoteId = await createNote(parentNoteId, comment.link_title, noteText);
 | 
			
		||||
 | 
			
		||||
            log.info("Reddit: Imported comment to note " + commentNoteId);
 | 
			
		||||
            importedComments++;
 | 
			
		||||
 | 
			
		||||
            await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind);
 | 
			
		||||
            await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id));
 | 
			
		||||
            await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if there have been no imported comments on this page, there shouldn't be any to import
 | 
			
		||||
    // on the next page since those are older
 | 
			
		||||
    if (listing.data.after && importedComments > 0) {
 | 
			
		||||
        importedComments += await importComments(rootNoteId, accountName, listing.data.after);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return importedComments;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let redditAccounts = [];
 | 
			
		||||
 | 
			
		||||
async function runImport() {
 | 
			
		||||
    const rootNoteId = await date_notes.getRootNoteId();
 | 
			
		||||
 | 
			
		||||
    // technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import
 | 
			
		||||
    // concurrently with sync
 | 
			
		||||
    await sync_mutex.doExclusively(async () => {
 | 
			
		||||
        let importedComments = 0;
 | 
			
		||||
 | 
			
		||||
        for (const account of redditAccounts) {
 | 
			
		||||
            importedComments += await importComments(rootNoteId, account);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.info(`Reddit: Imported ${importedComments} comments.`);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sql.dbReady.then(async () => {
 | 
			
		||||
    if (!config['Reddit'] || config['Reddit']['enabled'] !== true) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const redditAccountsStr = config['Reddit']['accounts'];
 | 
			
		||||
 | 
			
		||||
    if (!redditAccountsStr) {
 | 
			
		||||
        log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    redditAccounts = redditAccountsStr.split(",").map(s => s.trim());
 | 
			
		||||
 | 
			
		||||
    const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600);
 | 
			
		||||
 | 
			
		||||
    setInterval(runImport, pollingIntervalInSeconds * 1000);
 | 
			
		||||
    setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 358 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 252 B  | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/search-small.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/search-small.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 354 B  | 
@@ -1,21 +0,0 @@
 | 
			
		||||
const api = (function() {
 | 
			
		||||
    const $pluginButtons = $("#plugin-buttons");
 | 
			
		||||
 | 
			
		||||
    async function activateNote(notePath) {
 | 
			
		||||
        await noteTree.activateNode(notePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addButtonToToolbar(buttonId, button) {
 | 
			
		||||
        $("#" + buttonId).remove();
 | 
			
		||||
 | 
			
		||||
        button.attr('id', buttonId);
 | 
			
		||||
 | 
			
		||||
        $pluginButtons.append(button);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        addButtonToToolbar,
 | 
			
		||||
        activateNote
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const cloning = (function() {
 | 
			
		||||
    async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
 | 
			
		||||
        const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
 | 
			
		||||
            prefix: prefix
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!resp.success) {
 | 
			
		||||
            alert(resp.message);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await noteTree.reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // beware that first arg is noteId and second is noteTreeId!
 | 
			
		||||
    async function cloneNoteAfter(noteId, afterNoteTreeId) {
 | 
			
		||||
        const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId);
 | 
			
		||||
 | 
			
		||||
        if (!resp.success) {
 | 
			
		||||
            alert(resp.message);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await noteTree.reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        cloneNoteAfter,
 | 
			
		||||
        cloneNoteTo
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,164 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const contextMenu = (function() {
 | 
			
		||||
    const $tree = $("#tree");
 | 
			
		||||
 | 
			
		||||
    let clipboardIds = [];
 | 
			
		||||
    let clipboardMode = null;
 | 
			
		||||
 | 
			
		||||
    async function pasteAfter(node) {
 | 
			
		||||
        if (clipboardMode === 'cut') {
 | 
			
		||||
            const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
 | 
			
		||||
 | 
			
		||||
            await treeChanges.moveAfterNode(nodes, node);
 | 
			
		||||
 | 
			
		||||
            clipboardIds = [];
 | 
			
		||||
            clipboardMode = null;
 | 
			
		||||
        }
 | 
			
		||||
        else if (clipboardMode === 'copy') {
 | 
			
		||||
            for (const noteId of clipboardIds) {
 | 
			
		||||
                await cloning.cloneNoteAfter(noteId, node.data.noteTreeId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
 | 
			
		||||
        }
 | 
			
		||||
        else if (clipboardIds.length === 0) {
 | 
			
		||||
            // just do nothing
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throwError("Unrecognized clipboard mode=" + clipboardMode);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function pasteInto(node) {
 | 
			
		||||
        if (clipboardMode === 'cut') {
 | 
			
		||||
            const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
 | 
			
		||||
 | 
			
		||||
            await treeChanges.moveToNode(nodes, node);
 | 
			
		||||
 | 
			
		||||
            clipboardIds = [];
 | 
			
		||||
            clipboardMode = null;
 | 
			
		||||
        }
 | 
			
		||||
        else if (clipboardMode === 'copy') {
 | 
			
		||||
            for (const noteId of clipboardIds) {
 | 
			
		||||
                await cloning.cloneNoteTo(noteId, node.data.noteId);
 | 
			
		||||
            }
 | 
			
		||||
            // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
 | 
			
		||||
        }
 | 
			
		||||
        else if (clipboardIds.length === 0) {
 | 
			
		||||
            // just do nothing
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throwError("Unrecognized clipboard mode=" + mode);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function copy(nodes) {
 | 
			
		||||
        clipboardIds = nodes.map(node => node.data.noteId);
 | 
			
		||||
        clipboardMode = 'copy';
 | 
			
		||||
 | 
			
		||||
        showMessage("Note(s) have been copied into clipboard.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function cut(nodes) {
 | 
			
		||||
        clipboardIds = nodes.map(node => node.key);
 | 
			
		||||
        clipboardMode = 'cut';
 | 
			
		||||
 | 
			
		||||
        showMessage("Note(s) have been cut into clipboard.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const contextMenuSettings = {
 | 
			
		||||
        delegate: "span.fancytree-title",
 | 
			
		||||
        autoFocus: true,
 | 
			
		||||
        menu: [
 | 
			
		||||
            {title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
 | 
			
		||||
            {title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
 | 
			
		||||
            {title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
 | 
			
		||||
            {title: "----"},
 | 
			
		||||
            {title: "Edit tree prefix <kbd>F2</kbd>", cmd: "editTreePrefix", uiIcon: "ui-icon-pencil"},
 | 
			
		||||
            {title: "----"},
 | 
			
		||||
            {title: "Protect sub-tree", cmd: "protectSubTree", uiIcon: "ui-icon-locked"},
 | 
			
		||||
            {title: "Unprotect sub-tree", cmd: "unprotectSubTree", uiIcon: "ui-icon-unlocked"},
 | 
			
		||||
            {title: "----"},
 | 
			
		||||
            {title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
 | 
			
		||||
            {title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
 | 
			
		||||
            {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
            {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
            {title: "----"},
 | 
			
		||||
            {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
 | 
			
		||||
            {title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
 | 
			
		||||
            {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
 | 
			
		||||
 | 
			
		||||
        ],
 | 
			
		||||
        beforeOpen: (event, ui) => {
 | 
			
		||||
            const node = $.ui.fancytree.getNode(ui.target);
 | 
			
		||||
            // Modify menu entries depending on node status
 | 
			
		||||
            $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
 | 
			
		||||
            $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
 | 
			
		||||
 | 
			
		||||
            // Activate node on right-click
 | 
			
		||||
            node.setActive();
 | 
			
		||||
            // Disable tree keyboard handling
 | 
			
		||||
            ui.menu.prevKeyboard = node.tree.options.keyboard;
 | 
			
		||||
            node.tree.options.keyboard = false;
 | 
			
		||||
        },
 | 
			
		||||
        close: (event, ui) => {},
 | 
			
		||||
        select: (event, ui) => {
 | 
			
		||||
            const node = $.ui.fancytree.getNode(ui.target);
 | 
			
		||||
 | 
			
		||||
            if (ui.cmd === "insertNoteHere") {
 | 
			
		||||
                const parentNoteId = node.data.parentNoteId;
 | 
			
		||||
                const isProtected = treeUtils.getParentProtectedStatus(node);
 | 
			
		||||
 | 
			
		||||
                noteTree.createNote(node, parentNoteId, 'after', isProtected);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "insertChildNote") {
 | 
			
		||||
                noteTree.createNote(node, node.data.noteId, 'into');
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "editTreePrefix") {
 | 
			
		||||
                editTreePrefix.showDialog(node);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "protectSubTree") {
 | 
			
		||||
                protected_session.protectSubTree(node.data.noteId, true);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "unprotectSubTree") {
 | 
			
		||||
                protected_session.protectSubTree(node.data.noteId, false);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "copy") {
 | 
			
		||||
                copy(noteTree.getSelectedNodes());
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "cut") {
 | 
			
		||||
                cut(noteTree.getSelectedNodes());
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "pasteAfter") {
 | 
			
		||||
                pasteAfter(node);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "pasteInto") {
 | 
			
		||||
                pasteInto(node);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "delete") {
 | 
			
		||||
                treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "collapse-sub-tree") {
 | 
			
		||||
                noteTree.collapseTree(node);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "force-note-sync") {
 | 
			
		||||
                forceNoteSync(node.data.noteId);
 | 
			
		||||
            }
 | 
			
		||||
            else if (ui.cmd === "sort-alphabetically") {
 | 
			
		||||
                noteTree.sortAlphabetically(node.data.noteId);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                messaging.logError("Unknown command: " + ui.cmd);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        pasteAfter,
 | 
			
		||||
        pasteInto,
 | 
			
		||||
        cut,
 | 
			
		||||
        copy,
 | 
			
		||||
        contextMenuSettings
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,28 +1,31 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import cloningService from '../services/cloning.js';
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
import treeUtils from '../services/tree_utils.js';
 | 
			
		||||
import autocompleteService from '../services/autocomplete.js';
 | 
			
		||||
 | 
			
		||||
const addLink = (function() {
 | 
			
		||||
    const $dialog = $("#add-link-dialog");
 | 
			
		||||
    const $form = $("#add-link-form");
 | 
			
		||||
    const $autoComplete = $("#note-autocomplete");
 | 
			
		||||
    const $linkTitle = $("#link-title");
 | 
			
		||||
    const $clonePrefix = $("#clone-prefix");
 | 
			
		||||
    const $linkTitleFormGroup = $("#add-link-title-form-group");
 | 
			
		||||
    const $prefixFormGroup = $("#add-link-prefix-form-group");
 | 
			
		||||
    const $linkTypes = $("input[name='add-link-type']");
 | 
			
		||||
    const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
 | 
			
		||||
const $dialog = $("#add-link-dialog");
 | 
			
		||||
const $form = $("#add-link-form");
 | 
			
		||||
const $autoComplete = $("#note-autocomplete");
 | 
			
		||||
const $linkTitle = $("#link-title");
 | 
			
		||||
const $clonePrefix = $("#clone-prefix");
 | 
			
		||||
const $linkTitleFormGroup = $("#add-link-title-form-group");
 | 
			
		||||
const $prefixFormGroup = $("#add-link-prefix-form-group");
 | 
			
		||||
const $linkTypes = $("input[name='add-link-type']");
 | 
			
		||||
const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
 | 
			
		||||
 | 
			
		||||
    function setLinkType(linkType) {
 | 
			
		||||
function setLinkType(linkType) {
 | 
			
		||||
    $linkTypes.each(function () {
 | 
			
		||||
        $(this).prop('checked', $(this).val() === linkType);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    linkTypeChanged();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function showDialog() {
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
        if (noteEditor.getCurrentNoteType() === 'text') {
 | 
			
		||||
    if (noteDetailService.getCurrentNoteType() === 'text') {
 | 
			
		||||
        $linkTypeHtml.prop('disabled', false);
 | 
			
		||||
 | 
			
		||||
        setLinkType('html');
 | 
			
		||||
@@ -43,17 +46,17 @@ const addLink = (function() {
 | 
			
		||||
    $linkTitle.val('');
 | 
			
		||||
 | 
			
		||||
    function setDefaultLinkTitle(noteId) {
 | 
			
		||||
            const noteTitle = noteTree.getNoteTitle(noteId);
 | 
			
		||||
        const noteTitle = treeUtils.getNoteTitle(noteId);
 | 
			
		||||
 | 
			
		||||
        $linkTitle.val(noteTitle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $autoComplete.autocomplete({
 | 
			
		||||
            source: noteTree.getAutocompleteItems(),
 | 
			
		||||
        source: await autocompleteService.getAutocompleteItems(),
 | 
			
		||||
        minLength: 0,
 | 
			
		||||
        change: () => {
 | 
			
		||||
            const val = $autoComplete.val();
 | 
			
		||||
                const notePath = link.getNodePathFromLabel(val);
 | 
			
		||||
            const notePath = linkService.getNodePathFromLabel(val);
 | 
			
		||||
            if (!notePath) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
@@ -67,18 +70,18 @@ const addLink = (function() {
 | 
			
		||||
        // this is called when user goes through autocomplete list with keyboard
 | 
			
		||||
        // at this point the item isn't selected yet so we use supplied ui.item to see WHERE the cursor is
 | 
			
		||||
        focus: (event, ui) => {
 | 
			
		||||
                const notePath = link.getNodePathFromLabel(ui.item.value);
 | 
			
		||||
            const notePath = linkService.getNodePathFromLabel(ui.item.value);
 | 
			
		||||
            const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
            setDefaultLinkTitle(noteId);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
$form.submit(() => {
 | 
			
		||||
    const value = $autoComplete.val();
 | 
			
		||||
 | 
			
		||||
        const notePath = link.getNodePathFromLabel(value);
 | 
			
		||||
    const notePath = linkService.getNodePathFromLabel(value);
 | 
			
		||||
    const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
@@ -89,28 +92,28 @@ const addLink = (function() {
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog("close");
 | 
			
		||||
 | 
			
		||||
                link.addLinkToEditor(linkTitle, '#' + notePath);
 | 
			
		||||
            linkService.addLinkToEditor(linkTitle, '#' + notePath);
 | 
			
		||||
        }
 | 
			
		||||
        else if (linkType === 'selected-to-current') {
 | 
			
		||||
            const prefix = $clonePrefix.val();
 | 
			
		||||
 | 
			
		||||
                cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
 | 
			
		||||
            cloningService.cloneNoteTo(noteId, noteDetailService.getCurrentNoteId(), prefix);
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog("close");
 | 
			
		||||
        }
 | 
			
		||||
        else if (linkType === 'current-to-selected') {
 | 
			
		||||
            const prefix = $clonePrefix.val();
 | 
			
		||||
 | 
			
		||||
                cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
 | 
			
		||||
            cloningService.cloneNoteTo(noteDetailService.getCurrentNoteId(), noteId, prefix);
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog("close");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    function linkTypeChanged() {
 | 
			
		||||
function linkTypeChanged() {
 | 
			
		||||
    const value = $linkTypes.filter(":checked").val();
 | 
			
		||||
 | 
			
		||||
    if (value === 'html') {
 | 
			
		||||
@@ -121,17 +124,10 @@ const addLink = (function() {
 | 
			
		||||
        $linkTitleFormGroup.hide();
 | 
			
		||||
        $prefixFormGroup.show();
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    $linkTypes.change(linkTypeChanged);
 | 
			
		||||
$linkTypes.change(linkTypeChanged);
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+l', e => {
 | 
			
		||||
        showDialog();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
@@ -1,224 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const attributesDialog = (function() {
 | 
			
		||||
    const $dialog = $("#attributes-dialog");
 | 
			
		||||
    const $saveAttributesButton = $("#save-attributes-button");
 | 
			
		||||
    const $attributesBody = $('#attributes-table tbody');
 | 
			
		||||
 | 
			
		||||
    const attributesModel = new AttributesModel();
 | 
			
		||||
    let attributeNames = [];
 | 
			
		||||
 | 
			
		||||
    function AttributesModel() {
 | 
			
		||||
        const self = this;
 | 
			
		||||
 | 
			
		||||
        this.attributes = ko.observableArray();
 | 
			
		||||
 | 
			
		||||
        this.loadAttributes = async function() {
 | 
			
		||||
            const noteId = noteEditor.getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
            const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
			
		||||
 | 
			
		||||
            self.attributes(attributes.map(ko.observable));
 | 
			
		||||
 | 
			
		||||
            addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
            attributeNames = await server.get('attributes/names');
 | 
			
		||||
 | 
			
		||||
            // attribute might not be rendered immediatelly so could not focus
 | 
			
		||||
            setTimeout(() => $(".attribute-name:last").focus(), 100);
 | 
			
		||||
 | 
			
		||||
            $attributesBody.sortable({
 | 
			
		||||
                handle: '.handle',
 | 
			
		||||
                containment: $attributesBody,
 | 
			
		||||
                update: function() {
 | 
			
		||||
                    let position = 0;
 | 
			
		||||
 | 
			
		||||
                    // we need to update positions by searching in the DOM, because order of the
 | 
			
		||||
                    // attributes in the viewmodel (self.attributes()) stays the same
 | 
			
		||||
                    $attributesBody.find('input[name="position"]').each(function() {
 | 
			
		||||
                        const attr = self.getTargetAttribute(this);
 | 
			
		||||
 | 
			
		||||
                        attr().position = position++;
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.deleteAttribute = function(data, event) {
 | 
			
		||||
            const attr = self.getTargetAttribute(event.target);
 | 
			
		||||
            const attrData = attr();
 | 
			
		||||
 | 
			
		||||
            if (attrData) {
 | 
			
		||||
                attrData.isDeleted = 1;
 | 
			
		||||
 | 
			
		||||
                attr(attrData);
 | 
			
		||||
 | 
			
		||||
                addLastEmptyRow();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        function isValid() {
 | 
			
		||||
            for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
 | 
			
		||||
                if (self.isEmptyName(i)) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.save = async function() {
 | 
			
		||||
            // we need to defocus from input (in case of enter-triggered save) because value is updated
 | 
			
		||||
            // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
 | 
			
		||||
            // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
 | 
			
		||||
            $saveAttributesButton.focus();
 | 
			
		||||
 | 
			
		||||
            if (!isValid()) {
 | 
			
		||||
                alert("Please fix all validation errors and try saving again.");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const noteId = noteEditor.getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
            const attributesToSave = self.attributes()
 | 
			
		||||
                .map(attr => attr())
 | 
			
		||||
                .filter(attr => attr.attributeId !== "" || attr.name !== "");
 | 
			
		||||
 | 
			
		||||
            const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
 | 
			
		||||
 | 
			
		||||
            self.attributes(attributes.map(ko.observable));
 | 
			
		||||
 | 
			
		||||
            addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
            showMessage("Attributes have been saved.");
 | 
			
		||||
 | 
			
		||||
            noteEditor.loadAttributeList();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        function addLastEmptyRow() {
 | 
			
		||||
            const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
 | 
			
		||||
            const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
 | 
			
		||||
 | 
			
		||||
            if (!last || last.name.trim() !== "" || last.value !== "") {
 | 
			
		||||
                self.attributes.push(ko.observable({
 | 
			
		||||
                    attributeId: '',
 | 
			
		||||
                    name: '',
 | 
			
		||||
                    value: '',
 | 
			
		||||
                    isDeleted: 0,
 | 
			
		||||
                    position: 0
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.attributeChanged = function (data, event) {
 | 
			
		||||
            addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
            const attr = self.getTargetAttribute(event.target);
 | 
			
		||||
 | 
			
		||||
            attr.valueHasMutated();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.isNotUnique = function(index) {
 | 
			
		||||
            const cur = self.attributes()[index]();
 | 
			
		||||
 | 
			
		||||
            if (cur.name.trim() === "") {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
 | 
			
		||||
                const attr = attrs[i]();
 | 
			
		||||
 | 
			
		||||
                if (index !== i && cur.name === attr.name) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.isEmptyName = function(index) {
 | 
			
		||||
            const cur = self.attributes()[index]();
 | 
			
		||||
 | 
			
		||||
            return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.getTargetAttribute = function(target) {
 | 
			
		||||
            const context = ko.contextFor(target);
 | 
			
		||||
            const index = context.$index();
 | 
			
		||||
 | 
			
		||||
            return self.attributes()[index];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
        glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
        await attributesModel.loadAttributes();
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog({
 | 
			
		||||
            modal: true,
 | 
			
		||||
            width: 800,
 | 
			
		||||
            height: 500
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'alt+a', e => {
 | 
			
		||||
        showDialog();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
 | 
			
		||||
 | 
			
		||||
    $(document).on('focus', '.attribute-name', function (e) {
 | 
			
		||||
        if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
			
		||||
            $(this).autocomplete({
 | 
			
		||||
                // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
			
		||||
                // because we have overriden filter() function in init.js
 | 
			
		||||
                source: attributeNames.map(attr => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        label: attr,
 | 
			
		||||
                        value: attr
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
                minLength: 0
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $(this).autocomplete("search", $(this).val());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).on('focus', '.attribute-value', async function (e) {
 | 
			
		||||
        if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
			
		||||
            const attributeName = $(this).parent().parent().find('.attribute-name').val();
 | 
			
		||||
 | 
			
		||||
            if (attributeName.trim() === "") {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
 | 
			
		||||
 | 
			
		||||
            if (attributeValues.length === 0) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $(this).autocomplete({
 | 
			
		||||
                // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
			
		||||
                // because we have overriden filter() function in init.js
 | 
			
		||||
                source: attributeValues.map(attr => {
 | 
			
		||||
                    return {
 | 
			
		||||
                        label: attr,
 | 
			
		||||
                        value: attr
 | 
			
		||||
                    }
 | 
			
		||||
                }),
 | 
			
		||||
                minLength: 0
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $(this).autocomplete("search", $(this).val());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										51
									
								
								src/public/javascripts/dialogs/branch_prefix.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/public/javascripts/dialogs/branch_prefix.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import treeCache from "../services/tree_cache.js";
 | 
			
		||||
import treeUtils from "../services/tree_utils.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#edit-tree-prefix-dialog");
 | 
			
		||||
const $form = $("#edit-tree-prefix-form");
 | 
			
		||||
const $treePrefixInput = $("#tree-prefix-input");
 | 
			
		||||
const $noteTitle = $('#tree-prefix-note-title');
 | 
			
		||||
 | 
			
		||||
let branchId;
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    await $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
        width: 500
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const currentNode = treeService.getCurrentNode();
 | 
			
		||||
 | 
			
		||||
    branchId = currentNode.data.branchId;
 | 
			
		||||
    const branch = await treeCache.getBranch(branchId);
 | 
			
		||||
 | 
			
		||||
    $treePrefixInput.val(branch.prefix).focus();
 | 
			
		||||
 | 
			
		||||
    const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
 | 
			
		||||
    $noteTitle.html(noteTitle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function savePrefix() {
 | 
			
		||||
    const prefix = $treePrefixInput.val();
 | 
			
		||||
 | 
			
		||||
    await server.put('branches/' + branchId + '/set-prefix', { prefix: prefix });
 | 
			
		||||
 | 
			
		||||
    await treeService.setPrefix(branchId, prefix);
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog("close");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$form.submit(() => {
 | 
			
		||||
    savePrefix();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
};
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const editTreePrefix = (function() {
 | 
			
		||||
    const $dialog = $("#edit-tree-prefix-dialog");
 | 
			
		||||
    const $form = $("#edit-tree-prefix-form");
 | 
			
		||||
    const $treePrefixInput = $("#tree-prefix-input");
 | 
			
		||||
    const $noteTitle = $('#tree-prefix-note-title');
 | 
			
		||||
 | 
			
		||||
    let noteTreeId;
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
        glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
        await $dialog.dialog({
 | 
			
		||||
            modal: true,
 | 
			
		||||
            width: 500
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const currentNode = noteTree.getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        noteTreeId = currentNode.data.noteTreeId;
 | 
			
		||||
 | 
			
		||||
        $treePrefixInput.val(currentNode.data.prefix).focus();
 | 
			
		||||
 | 
			
		||||
        const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
 | 
			
		||||
        $noteTitle.html(noteTitle);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        const prefix = $treePrefixInput.val();
 | 
			
		||||
 | 
			
		||||
        server.put('tree/' + noteTreeId + '/set-prefix', {
 | 
			
		||||
            prefix: prefix
 | 
			
		||||
        }).then(() => noteTree.setPrefix(noteTreeId, prefix));
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog("close");
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
 | 
			
		||||
const eventLog = (function() {
 | 
			
		||||
    const $dialog = $("#event-log-dialog");
 | 
			
		||||
    const $list = $("#event-log-list");
 | 
			
		||||
const $dialog = $("#event-log-dialog");
 | 
			
		||||
const $list = $("#event-log-list");
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
@@ -18,10 +19,10 @@ const eventLog = (function() {
 | 
			
		||||
    $list.html('');
 | 
			
		||||
 | 
			
		||||
    for (const event of result) {
 | 
			
		||||
            const dateTime = formatDateTime(parseDate(event.dateAdded));
 | 
			
		||||
        const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
 | 
			
		||||
 | 
			
		||||
        if (event.noteId) {
 | 
			
		||||
                const noteLink = link.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
            const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
 | 
			
		||||
            event.comment = event.comment.replace('<note>', noteLink);
 | 
			
		||||
        }
 | 
			
		||||
@@ -30,9 +31,8 @@ const eventLog = (function() {
 | 
			
		||||
 | 
			
		||||
        $list.append(eventEl);
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import autocompleteService from '../services/autocomplete.js';
 | 
			
		||||
 | 
			
		||||
const jumpToNote = (function() {
 | 
			
		||||
    const $dialog = $("#jump-to-note-dialog");
 | 
			
		||||
    const $autoComplete = $("#jump-to-note-autocomplete");
 | 
			
		||||
    const $form = $("#jump-to-note-form");
 | 
			
		||||
const $dialog = $("#jump-to-note-dialog");
 | 
			
		||||
const $autoComplete = $("#jump-to-note-autocomplete");
 | 
			
		||||
const $form = $("#jump-to-note-form");
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $autoComplete.val('');
 | 
			
		||||
@@ -16,41 +18,32 @@ const jumpToNote = (function() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await $autoComplete.autocomplete({
 | 
			
		||||
            source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
 | 
			
		||||
            minLength: 0
 | 
			
		||||
        source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
 | 
			
		||||
        minLength: 1
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function getSelectedNotePath() {
 | 
			
		||||
function getSelectedNotePath() {
 | 
			
		||||
    const val = $autoComplete.val();
 | 
			
		||||
        return link.getNodePathFromLabel(val);
 | 
			
		||||
    }
 | 
			
		||||
    return linkService.getNodePathFromLabel(val);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function goToNote() {
 | 
			
		||||
function goToNote() {
 | 
			
		||||
    const notePath = getSelectedNotePath();
 | 
			
		||||
 | 
			
		||||
    if (notePath) {
 | 
			
		||||
            noteTree.activateNode(notePath);
 | 
			
		||||
        treeService.activateNode(notePath);
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog('close');
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+j', e => {
 | 
			
		||||
        showDialog();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        const action = $dialog.find("button:focus").val();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$form.submit(() => {
 | 
			
		||||
    goToNote();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										223
									
								
								src/public/javascripts/dialogs/labels.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/public/javascripts/dialogs/labels.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import infoService from "../services/info.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#labels-dialog");
 | 
			
		||||
const $saveLabelsButton = $("#save-labels-button");
 | 
			
		||||
const $labelsBody = $('#labels-table tbody');
 | 
			
		||||
 | 
			
		||||
const labelsModel = new LabelsModel();
 | 
			
		||||
let labelNames = [];
 | 
			
		||||
 | 
			
		||||
function LabelsModel() {
 | 
			
		||||
    const self = this;
 | 
			
		||||
 | 
			
		||||
    this.labels = ko.observableArray();
 | 
			
		||||
 | 
			
		||||
    this.updateLabelPositions = function() {
 | 
			
		||||
        let position = 0;
 | 
			
		||||
 | 
			
		||||
        // we need to update positions by searching in the DOM, because order of the
 | 
			
		||||
        // labels in the viewmodel (self.labels()) stays the same
 | 
			
		||||
        $labelsBody.find('input[name="position"]').each(function() {
 | 
			
		||||
            const label = self.getTargetLabel(this);
 | 
			
		||||
 | 
			
		||||
            label().position = position++;
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.loadLabels = async function() {
 | 
			
		||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
        const labels = await server.get('notes/' + noteId + '/labels');
 | 
			
		||||
 | 
			
		||||
        self.labels(labels.map(ko.observable));
 | 
			
		||||
 | 
			
		||||
        addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
        labelNames = await server.get('labels/names');
 | 
			
		||||
 | 
			
		||||
        // label might not be rendered immediatelly so could not focus
 | 
			
		||||
        setTimeout(() => $(".label-name:last").focus(), 100);
 | 
			
		||||
 | 
			
		||||
        $labelsBody.sortable({
 | 
			
		||||
            handle: '.handle',
 | 
			
		||||
            containment: $labelsBody,
 | 
			
		||||
            update: this.updateLabelPositions
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.deleteLabel = function(data, event) {
 | 
			
		||||
        const label = self.getTargetLabel(event.target);
 | 
			
		||||
        const labelData = label();
 | 
			
		||||
 | 
			
		||||
        if (labelData) {
 | 
			
		||||
            labelData.isDeleted = 1;
 | 
			
		||||
 | 
			
		||||
            label(labelData);
 | 
			
		||||
 | 
			
		||||
            addLastEmptyRow();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function isValid() {
 | 
			
		||||
        for (let labels = self.labels(), i = 0; i < labels.length; i++) {
 | 
			
		||||
            if (self.isEmptyName(i)) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.save = async function() {
 | 
			
		||||
        // we need to defocus from input (in case of enter-triggered save) because value is updated
 | 
			
		||||
        // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
 | 
			
		||||
        // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
 | 
			
		||||
        $saveLabelsButton.focus();
 | 
			
		||||
 | 
			
		||||
        if (!isValid()) {
 | 
			
		||||
            alert("Please fix all validation errors and try saving again.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.updateLabelPositions();
 | 
			
		||||
 | 
			
		||||
        const noteId = noteDetailService.getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
        const labelsToSave = self.labels()
 | 
			
		||||
            .map(label => label())
 | 
			
		||||
            .filter(label => label.labelId !== "" || label.name !== "");
 | 
			
		||||
 | 
			
		||||
        const labels = await server.put('notes/' + noteId + '/labels', labelsToSave);
 | 
			
		||||
 | 
			
		||||
        self.labels(labels.map(ko.observable));
 | 
			
		||||
 | 
			
		||||
        addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
        infoService.showMessage("Labels have been saved.");
 | 
			
		||||
 | 
			
		||||
        noteDetailService.loadLabelList();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function addLastEmptyRow() {
 | 
			
		||||
        const labels = self.labels().filter(attr => attr().isDeleted === 0);
 | 
			
		||||
        const last = labels.length === 0 ? null : labels[labels.length - 1]();
 | 
			
		||||
 | 
			
		||||
        if (!last || last.name.trim() !== "" || last.value !== "") {
 | 
			
		||||
            self.labels.push(ko.observable({
 | 
			
		||||
                labelId: '',
 | 
			
		||||
                name: '',
 | 
			
		||||
                value: '',
 | 
			
		||||
                isDeleted: 0,
 | 
			
		||||
                position: 0
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.labelChanged = function (data, event) {
 | 
			
		||||
        addLastEmptyRow();
 | 
			
		||||
 | 
			
		||||
        const label = self.getTargetLabel(event.target);
 | 
			
		||||
 | 
			
		||||
        label.valueHasMutated();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.isNotUnique = function(index) {
 | 
			
		||||
        const cur = self.labels()[index]();
 | 
			
		||||
 | 
			
		||||
        if (cur.name.trim() === "") {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (let labels = self.labels(), i = 0; i < labels.length; i++) {
 | 
			
		||||
            const label = labels[i]();
 | 
			
		||||
 | 
			
		||||
            if (index !== i && cur.name === label.name) {
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.isEmptyName = function(index) {
 | 
			
		||||
        const cur = self.labels()[index]();
 | 
			
		||||
 | 
			
		||||
        return cur.name.trim() === "" && (cur.labelId !== "" || cur.value !== "");
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.getTargetLabel = function(target) {
 | 
			
		||||
        const context = ko.contextFor(target);
 | 
			
		||||
        const index = context.$index();
 | 
			
		||||
 | 
			
		||||
        return self.labels()[index];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    await labelsModel.loadLabels();
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
        width: 800,
 | 
			
		||||
        height: 500
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ko.applyBindings(labelsModel, document.getElementById('labels-dialog'));
 | 
			
		||||
 | 
			
		||||
$(document).on('focus', '.label-name', function (e) {
 | 
			
		||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
			
		||||
        $(this).autocomplete({
 | 
			
		||||
            // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
			
		||||
            // because we have overriden filter() function in autocomplete.js
 | 
			
		||||
            source: labelNames.map(label => {
 | 
			
		||||
                return {
 | 
			
		||||
                    label: label,
 | 
			
		||||
                    value: label
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
            minLength: 0
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(this).autocomplete("search", $(this).val());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).on('focus', '.label-value', async function (e) {
 | 
			
		||||
    if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
			
		||||
        const labelName = $(this).parent().parent().find('.label-name').val();
 | 
			
		||||
 | 
			
		||||
        if (labelName.trim() === "") {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const labelValues = await server.get('labels/values/' + encodeURIComponent(labelName));
 | 
			
		||||
 | 
			
		||||
        if (labelValues.length === 0) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $(this).autocomplete({
 | 
			
		||||
            // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
			
		||||
            // because we have overriden filter() function in autocomplete.js
 | 
			
		||||
            source: labelValues.map(label => {
 | 
			
		||||
                return {
 | 
			
		||||
                    label: label,
 | 
			
		||||
                    value: label
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
            minLength: 0
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(this).autocomplete("search", $(this).val());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
};
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteHistory = (function() {
 | 
			
		||||
    const $dialog = $("#note-history-dialog");
 | 
			
		||||
    const $list = $("#note-history-list");
 | 
			
		||||
    const $content = $("#note-history-content");
 | 
			
		||||
    const $title = $("#note-history-title");
 | 
			
		||||
 | 
			
		||||
    let historyItems = [];
 | 
			
		||||
 | 
			
		||||
    async function showCurrentNoteHistory() {
 | 
			
		||||
        await showNoteHistoryDialog(noteEditor.getCurrentNoteId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function showNoteHistoryDialog(noteId, noteRevisionId) {
 | 
			
		||||
        glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog({
 | 
			
		||||
            modal: true,
 | 
			
		||||
            width: 800,
 | 
			
		||||
            height: 700
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $list.empty();
 | 
			
		||||
        $content.empty();
 | 
			
		||||
 | 
			
		||||
        historyItems = await server.get('notes-history/' + noteId);
 | 
			
		||||
 | 
			
		||||
        for (const item of historyItems) {
 | 
			
		||||
            const dateModified = parseDate(item.dateModifiedFrom);
 | 
			
		||||
 | 
			
		||||
            $list.append($('<option>', {
 | 
			
		||||
                value: item.noteRevisionId,
 | 
			
		||||
                text: formatDateTime(dateModified)
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (historyItems.length > 0) {
 | 
			
		||||
            if (!noteRevisionId) {
 | 
			
		||||
                noteRevisionId = $list.find("option:first").val();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $list.val(noteRevisionId).trigger('change');
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $title.text("No history for this note yet...");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'alt+h', e => {
 | 
			
		||||
        showCurrentNoteHistory();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $list.on('change', () => {
 | 
			
		||||
        const optVal = $list.find(":selected").val();
 | 
			
		||||
 | 
			
		||||
        const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
 | 
			
		||||
 | 
			
		||||
        $title.html(historyItem.title);
 | 
			
		||||
        $content.html(historyItem.content);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).on('click', "a[action='note-history']", event => {
 | 
			
		||||
        const linkEl = $(event.target);
 | 
			
		||||
        const noteId = linkEl.attr('note-path');
 | 
			
		||||
        const noteRevisionId = linkEl.attr('note-history-id');
 | 
			
		||||
 | 
			
		||||
        showNoteHistoryDialog(noteId, noteRevisionId);
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        showCurrentNoteHistory
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										78
									
								
								src/public/javascripts/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/public/javascripts/dialogs/note_revisions.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#note-revisions-dialog");
 | 
			
		||||
const $list = $("#note-revision-list");
 | 
			
		||||
const $content = $("#note-revision-content");
 | 
			
		||||
const $title = $("#note-revision-title");
 | 
			
		||||
 | 
			
		||||
let revisionItems = [];
 | 
			
		||||
 | 
			
		||||
async function showCurrentNoteRevisions() {
 | 
			
		||||
    await showNoteRevisionsDialog(noteDetailService.getCurrentNoteId());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showNoteRevisionsDialog(noteId, noteRevisionId) {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
        width: 800,
 | 
			
		||||
        height: 700
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $list.empty();
 | 
			
		||||
    $content.empty();
 | 
			
		||||
 | 
			
		||||
    revisionItems = await server.get('notes/' + noteId + '/revisions');
 | 
			
		||||
 | 
			
		||||
    for (const item of revisionItems) {
 | 
			
		||||
        const dateModified = utils.parseDate(item.dateModifiedFrom);
 | 
			
		||||
 | 
			
		||||
        $list.append($('<option>', {
 | 
			
		||||
            value: item.noteRevisionId,
 | 
			
		||||
            text: utils.formatDateTime(dateModified)
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (revisionItems.length > 0) {
 | 
			
		||||
        if (!noteRevisionId) {
 | 
			
		||||
            noteRevisionId = $list.find("option:first").val();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $list.val(noteRevisionId).trigger('change');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $title.text("No revisions for this note yet...");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$list.on('change', () => {
 | 
			
		||||
    const optVal = $list.find(":selected").val();
 | 
			
		||||
 | 
			
		||||
    const revisionItem = revisionItems.find(r => r.noteRevisionId === optVal);
 | 
			
		||||
 | 
			
		||||
    $title.html(revisionItem.title);
 | 
			
		||||
 | 
			
		||||
    if (revisionItem.type === 'text') {
 | 
			
		||||
        $content.html(revisionItem.content);
 | 
			
		||||
    }
 | 
			
		||||
    else if (revisionItem.type === 'code') {
 | 
			
		||||
        $content.html($("<pre>").text(revisionItem.content));
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).on('click', "a[action='note-revision']", event => {
 | 
			
		||||
    const linkEl = $(event.target);
 | 
			
		||||
    const noteId = linkEl.attr('note-path');
 | 
			
		||||
    const noteRevisionId = linkEl.attr('note-revision-id');
 | 
			
		||||
 | 
			
		||||
    showNoteRevisionsDialog(noteId, noteRevisionId);
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showCurrentNoteRevisions
 | 
			
		||||
};
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import noteDetailService from '../services/note_detail.js';
 | 
			
		||||
 | 
			
		||||
const noteSource = (function() {
 | 
			
		||||
    const $dialog = $("#note-source-dialog");
 | 
			
		||||
    const $noteSource = $("#note-source");
 | 
			
		||||
const $dialog = $("#note-source-dialog");
 | 
			
		||||
const $noteSource = $("#note-source");
 | 
			
		||||
 | 
			
		||||
    function showDialog() {
 | 
			
		||||
function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
@@ -13,24 +12,24 @@ const noteSource = (function() {
 | 
			
		||||
        height: 500
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
        const noteText = noteEditor.getCurrentNote().detail.content;
 | 
			
		||||
    const noteText = noteDetailService.getCurrentNote().content;
 | 
			
		||||
 | 
			
		||||
    $noteSource.text(formatHtml(noteText));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function formatHtml(str) {
 | 
			
		||||
function formatHtml(str) {
 | 
			
		||||
    const div = document.createElement('div');
 | 
			
		||||
    div.innerHTML = str.trim();
 | 
			
		||||
 | 
			
		||||
    return formatNode(div, 0).innerHTML.trim();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function formatNode(node, level) {
 | 
			
		||||
function formatNode(node, level) {
 | 
			
		||||
    const indentBefore = new Array(level++ + 1).join('  ');
 | 
			
		||||
    const indentAfter  = new Array(level - 1).join('  ');
 | 
			
		||||
    let textNode;
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < node.children.length; i++) {
 | 
			
		||||
    for (const i = 0; i < node.children.length; i++) {
 | 
			
		||||
        textNode = document.createTextNode('\n' + indentBefore);
 | 
			
		||||
        node.insertBefore(textNode, node.children[i]);
 | 
			
		||||
 | 
			
		||||
@@ -43,15 +42,8 @@ const noteSource = (function() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return node;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+u', e => {
 | 
			
		||||
        showDialog();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
@@ -1,19 +1,23 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const settings = (function() {
 | 
			
		||||
    const $dialog = $("#settings-dialog");
 | 
			
		||||
    const $tabs = $("#settings-tabs");
 | 
			
		||||
import protectedSessionHolder from '../services/protected_session_holder.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import infoService from "../services/info.js";
 | 
			
		||||
 | 
			
		||||
    const settingModules = [];
 | 
			
		||||
const $dialog = $("#options-dialog");
 | 
			
		||||
const $tabs = $("#options-tabs");
 | 
			
		||||
 | 
			
		||||
    function addModule(module) {
 | 
			
		||||
        settingModules.push(module);
 | 
			
		||||
    }
 | 
			
		||||
const tabHandlers = [];
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
function addTabHandler(handler) {
 | 
			
		||||
    tabHandlers.push(handler);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
        const settings = await server.get('settings');
 | 
			
		||||
    const options = await server.get('options');
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
        modal: true,
 | 
			
		||||
@@ -22,36 +26,31 @@ const settings = (function() {
 | 
			
		||||
 | 
			
		||||
    $tabs.tabs();
 | 
			
		||||
 | 
			
		||||
        for (const module of settingModules) {
 | 
			
		||||
            if (module.settingsLoaded) {
 | 
			
		||||
                module.settingsLoaded(settings);
 | 
			
		||||
            }
 | 
			
		||||
    for (const handler of tabHandlers) {
 | 
			
		||||
        if (handler.optionsLoaded) {
 | 
			
		||||
            handler.optionsLoaded(options);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    async function saveSettings(settingName, settingValue) {
 | 
			
		||||
        await server.post('settings', {
 | 
			
		||||
            name: settingName,
 | 
			
		||||
            value: settingValue
 | 
			
		||||
        });
 | 
			
		||||
async function saveOptions(optionName, optionValue) {
 | 
			
		||||
    await server.put('options/' + encodeURIComponent(optionName) + '/' + encodeURIComponent(optionValue));
 | 
			
		||||
 | 
			
		||||
        showMessage("Settings change have been saved.");
 | 
			
		||||
    }
 | 
			
		||||
    infoService.showMessage("Options change have been saved.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog,
 | 
			
		||||
        saveSettings,
 | 
			
		||||
        addModule
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
    saveOptions
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
settings.addModule((function() {
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $form = $("#change-password-form");
 | 
			
		||||
    const $oldPassword = $("#old-password");
 | 
			
		||||
    const $newPassword1 = $("#new-password1");
 | 
			
		||||
    const $newPassword2 = $("#new-password2");
 | 
			
		||||
 | 
			
		||||
    function settingsLoaded(settings) {
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
@@ -76,10 +75,10 @@ settings.addModule((function() {
 | 
			
		||||
                alert("Password has been changed. Trilium will be reloaded after you press OK.");
 | 
			
		||||
 | 
			
		||||
                // password changed so current protected session is invalid and needs to be cleared
 | 
			
		||||
                protected_session.resetProtectedSession();
 | 
			
		||||
                protectedSessionHolder.resetProtectedSession();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                showError(result.message);
 | 
			
		||||
                infoService.showError(result.message);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -87,55 +86,55 @@ settings.addModule((function() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        settingsLoaded
 | 
			
		||||
        optionsLoaded
 | 
			
		||||
    };
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
settings.addModule((function() {
 | 
			
		||||
addTabHandler((function() {
 | 
			
		||||
    const $form = $("#protected-session-timeout-form");
 | 
			
		||||
    const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
 | 
			
		||||
    const settingName = 'protected_session_timeout';
 | 
			
		||||
    const optionName = 'protectedSessionTimeout';
 | 
			
		||||
 | 
			
		||||
    function settingsLoaded(settings) {
 | 
			
		||||
        $protectedSessionTimeout.val(settings[settingName]);
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $protectedSessionTimeout.val(options[optionName]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        const protectedSessionTimeout = $protectedSessionTimeout.val();
 | 
			
		||||
 | 
			
		||||
        settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
 | 
			
		||||
            protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
 | 
			
		||||
        saveOptions(optionName, protectedSessionTimeout).then(() => {
 | 
			
		||||
            protectedSessionHolder.setProtectedSessionTimeout(protectedSessionTimeout);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        settingsLoaded
 | 
			
		||||
        optionsLoaded
 | 
			
		||||
    };
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
settings.addModule((function () {
 | 
			
		||||
    const $form = $("#history-snapshot-time-interval-form");
 | 
			
		||||
    const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
 | 
			
		||||
    const settingName = 'history_snapshot_time_interval';
 | 
			
		||||
addTabHandler((function () {
 | 
			
		||||
    const $form = $("#note-revision-snapshot-time-interval-form");
 | 
			
		||||
    const $timeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
 | 
			
		||||
    const optionName = 'noteRevisionSnapshotTimeInterval';
 | 
			
		||||
 | 
			
		||||
    function settingsLoaded(settings) {
 | 
			
		||||
        $timeInterval.val(settings[settingName]);
 | 
			
		||||
    function optionsLoaded(options) {
 | 
			
		||||
        $timeInterval.val(options[optionName]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $form.submit(() => {
 | 
			
		||||
        settings.saveSettings(settingName, $timeInterval.val());
 | 
			
		||||
        saveOptions(optionName, $timeInterval.val());
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        settingsLoaded
 | 
			
		||||
        optionsLoaded
 | 
			
		||||
    };
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
settings.addModule((async function () {
 | 
			
		||||
addTabHandler((async function () {
 | 
			
		||||
    const $appVersion = $("#app-version");
 | 
			
		||||
    const $dbVersion = $("#db-version");
 | 
			
		||||
    const $buildDate = $("#build-date");
 | 
			
		||||
@@ -143,16 +142,16 @@ settings.addModule((async function () {
 | 
			
		||||
 | 
			
		||||
    const appInfo = await server.get('app-info');
 | 
			
		||||
 | 
			
		||||
    $appVersion.html(appInfo.app_version);
 | 
			
		||||
    $dbVersion.html(appInfo.db_version);
 | 
			
		||||
    $buildDate.html(appInfo.build_date);
 | 
			
		||||
    $buildRevision.html(appInfo.build_revision);
 | 
			
		||||
    $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
 | 
			
		||||
    $appVersion.html(appInfo.appVersion);
 | 
			
		||||
    $dbVersion.html(appInfo.dbVersion);
 | 
			
		||||
    $buildDate.html(appInfo.buildDate);
 | 
			
		||||
    $buildRevision.html(appInfo.buildRevision);
 | 
			
		||||
    $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.buildRevision);
 | 
			
		||||
 | 
			
		||||
    return {};
 | 
			
		||||
})());
 | 
			
		||||
 | 
			
		||||
settings.addModule((async function () {
 | 
			
		||||
addTabHandler((async function () {
 | 
			
		||||
    const $forceFullSyncButton = $("#force-full-sync-button");
 | 
			
		||||
    const $fillSyncRowsButton = $("#fill-sync-rows-button");
 | 
			
		||||
    const $anonymizeButton = $("#anonymize-button");
 | 
			
		||||
@@ -163,27 +162,27 @@ settings.addModule((async function () {
 | 
			
		||||
    $forceFullSyncButton.click(async () => {
 | 
			
		||||
        await server.post('sync/force-full-sync');
 | 
			
		||||
 | 
			
		||||
        showMessage("Full sync triggered");
 | 
			
		||||
        infoService.showMessage("Full sync triggered");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $fillSyncRowsButton.click(async () => {
 | 
			
		||||
        await server.post('sync/fill-sync-rows');
 | 
			
		||||
 | 
			
		||||
        showMessage("Sync rows filled successfully");
 | 
			
		||||
        infoService.showMessage("Sync rows filled successfully");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    $anonymizeButton.click(async () => {
 | 
			
		||||
        await server.post('anonymization/anonymize');
 | 
			
		||||
 | 
			
		||||
        showMessage("Created anonymized database");
 | 
			
		||||
        infoService.showMessage("Created anonymized database");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $cleanupSoftDeletedButton.click(async () => {
 | 
			
		||||
        if (confirm("Do you really want to clean up soft-deleted items?")) {
 | 
			
		||||
            await server.post('cleanup/cleanup-soft-deleted-items');
 | 
			
		||||
 | 
			
		||||
            showMessage("Soft deleted items have been cleaned up");
 | 
			
		||||
            infoService.showMessage("Soft deleted items have been cleaned up");
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -191,14 +190,14 @@ settings.addModule((async function () {
 | 
			
		||||
        if (confirm("Do you really want to clean up unused images?")) {
 | 
			
		||||
            await server.post('cleanup/cleanup-unused-images');
 | 
			
		||||
 | 
			
		||||
            showMessage("Unused images have been cleaned up");
 | 
			
		||||
            infoService.showMessage("Unused images have been cleaned up");
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $vacuumDatabaseButton.click(async () => {
 | 
			
		||||
        await server.post('cleanup/vacuum-database');
 | 
			
		||||
 | 
			
		||||
        showMessage("Database has been vacuumed");
 | 
			
		||||
        infoService.showMessage("Database has been vacuumed");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {};
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
 | 
			
		||||
const recentChanges = (function() {
 | 
			
		||||
    const $dialog = $("#recent-changes-dialog");
 | 
			
		||||
const $dialog = $("#recent-changes-dialog");
 | 
			
		||||
 | 
			
		||||
    async function showDialog() {
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
@@ -21,17 +22,17 @@ const recentChanges = (function() {
 | 
			
		||||
    for (const [dateDay, dayChanges] of groupedByDate) {
 | 
			
		||||
        const changesListEl = $('<ul>');
 | 
			
		||||
 | 
			
		||||
            const dayEl = $('<div>').append($('<b>').html(formatDate(dateDay))).append(changesListEl);
 | 
			
		||||
        const dayEl = $('<div>').append($('<b>').html(utils.formatDate(dateDay))).append(changesListEl);
 | 
			
		||||
 | 
			
		||||
        for (const change of dayChanges) {
 | 
			
		||||
                const formattedTime = formatTime(parseDate(change.dateModifiedTo));
 | 
			
		||||
            const formattedTime = utils.formatTime(utils.parseDate(change.dateModifiedTo));
 | 
			
		||||
 | 
			
		||||
            const revLink = $("<a>", {
 | 
			
		||||
                href: 'javascript:',
 | 
			
		||||
                text: 'rev'
 | 
			
		||||
                }).attr('action', 'note-history')
 | 
			
		||||
            }).attr('action', 'note-revision')
 | 
			
		||||
                .attr('note-path', change.noteId)
 | 
			
		||||
                    .attr('note-history-id', change.noteRevisionId);
 | 
			
		||||
                .attr('note-revision-id', change.noteRevisionId);
 | 
			
		||||
 | 
			
		||||
            let noteLink;
 | 
			
		||||
 | 
			
		||||
@@ -39,7 +40,7 @@ const recentChanges = (function() {
 | 
			
		||||
                noteLink = change.current_title;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                    noteLink = link.createNoteLink(change.noteId, change.title);
 | 
			
		||||
                noteLink = linkService.createNoteLink(change.noteId, change.title);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            changesListEl.append($('<li>')
 | 
			
		||||
@@ -50,14 +51,14 @@ const recentChanges = (function() {
 | 
			
		||||
 | 
			
		||||
        $dialog.append(dayEl);
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function groupByDate(result) {
 | 
			
		||||
function groupByDate(result) {
 | 
			
		||||
    const groupedByDate = new Map();
 | 
			
		||||
    const dayCache = {};
 | 
			
		||||
 | 
			
		||||
    for (const row of result) {
 | 
			
		||||
            let dateDay = parseDate(row.dateModifiedTo);
 | 
			
		||||
        let dateDay = utils.parseDate(row.dateModifiedTo);
 | 
			
		||||
        dateDay.setHours(0);
 | 
			
		||||
        dateDay.setMinutes(0);
 | 
			
		||||
        dateDay.setSeconds(0);
 | 
			
		||||
@@ -79,11 +80,8 @@ const recentChanges = (function() {
 | 
			
		||||
        groupedByDate.get(dateDay).push(row);
 | 
			
		||||
    }
 | 
			
		||||
    return groupedByDate;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'alt+r', showDialog);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
@@ -1,30 +1,48 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import messagingService from '../services/messaging.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import treeUtils from "../services/tree_utils.js";
 | 
			
		||||
 | 
			
		||||
const recentNotes = (function() {
 | 
			
		||||
    const $dialog = $("#recent-notes-dialog");
 | 
			
		||||
    const $searchInput = $('#recent-notes-search-input');
 | 
			
		||||
const $dialog = $("#recent-notes-dialog");
 | 
			
		||||
const $searchInput = $('#recent-notes-search-input');
 | 
			
		||||
 | 
			
		||||
    // list of recent note paths
 | 
			
		||||
    let list = [];
 | 
			
		||||
// list of recent note paths
 | 
			
		||||
let list = [];
 | 
			
		||||
 | 
			
		||||
    async function reload() {
 | 
			
		||||
async function reload() {
 | 
			
		||||
    const result = await server.get('recent-notes');
 | 
			
		||||
 | 
			
		||||
    list = result.map(r => r.notePath);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    function addRecentNote(noteTreeId, 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 === noteTree.getCurrentNotePath()) {
 | 
			
		||||
                const result = await server.put('recent-notes/' + noteTreeId + '/' + encodeURIComponent(notePath));
 | 
			
		||||
        if (notePath && notePath === treeService.getCurrentNotePath()) {
 | 
			
		||||
            const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
 | 
			
		||||
 | 
			
		||||
            list = result.map(r => r.notePath);
 | 
			
		||||
        }
 | 
			
		||||
    }, 1500);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getNoteTitle(notePath) {
 | 
			
		||||
    let noteTitle;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        noteTitle = await treeUtils.getNotePathTitle(notePath);
 | 
			
		||||
    }
 | 
			
		||||
    catch (e) {
 | 
			
		||||
        noteTitle = "[error - can't find note title]";
 | 
			
		||||
 | 
			
		||||
        messagingService.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showDialog() {
 | 
			
		||||
    return noteTitle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
@@ -37,30 +55,22 @@ const recentNotes = (function() {
 | 
			
		||||
    $searchInput.val('');
 | 
			
		||||
 | 
			
		||||
    // remove the current note
 | 
			
		||||
        const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
 | 
			
		||||
    const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
 | 
			
		||||
    const items = [];
 | 
			
		||||
 | 
			
		||||
    for (const notePath of recNotes) {
 | 
			
		||||
        items.push({
 | 
			
		||||
            label: await getNoteTitle(notePath),
 | 
			
		||||
            value: notePath
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $searchInput.autocomplete({
 | 
			
		||||
            source: recNotes.map(notePath => {
 | 
			
		||||
                let noteTitle;
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    noteTitle = noteTree.getNotePathTitle(notePath);
 | 
			
		||||
                }
 | 
			
		||||
                catch (e) {
 | 
			
		||||
                    noteTitle = "[error - can't find note title]";
 | 
			
		||||
 | 
			
		||||
                    messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    label: noteTitle,
 | 
			
		||||
                    value: notePath
 | 
			
		||||
                }
 | 
			
		||||
            }),
 | 
			
		||||
        source: items,
 | 
			
		||||
        minLength: 0,
 | 
			
		||||
        autoFocus: true,
 | 
			
		||||
        select: function (event, ui) {
 | 
			
		||||
                noteTree.activateNode(ui.item.value);
 | 
			
		||||
            treeService.activateNode(ui.item.value);
 | 
			
		||||
 | 
			
		||||
            $searchInput.autocomplete('destroy');
 | 
			
		||||
            $dialog.dialog('close');
 | 
			
		||||
@@ -84,19 +94,20 @@ const recentNotes = (function() {
 | 
			
		||||
            "ui-autocomplete": "recent-notes-autocomplete"
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
setTimeout(reload, 100);
 | 
			
		||||
 | 
			
		||||
messagingService.subscribeToMessages(syncData => {
 | 
			
		||||
    if (syncData.some(sync => sync.entityName === 'recent_notes')) {
 | 
			
		||||
        console.log(utils.now(), "Reloading recent notes because of background changes");
 | 
			
		||||
 | 
			
		||||
        reload();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+e', e => {
 | 
			
		||||
        showDialog();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog,
 | 
			
		||||
    addRecentNote,
 | 
			
		||||
    reload
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
@@ -1,15 +1,17 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import libraryLoader from '../services/library_loader.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
import infoService from "../services/info.js";
 | 
			
		||||
 | 
			
		||||
const sqlConsole = (function() {
 | 
			
		||||
    const $dialog = $("#sql-console-dialog");
 | 
			
		||||
    const $query = $('#sql-console-query');
 | 
			
		||||
    const $executeButton = $('#sql-console-execute');
 | 
			
		||||
    const $resultHead = $('#sql-console-results thead');
 | 
			
		||||
    const $resultBody = $('#sql-console-results tbody');
 | 
			
		||||
const $dialog = $("#sql-console-dialog");
 | 
			
		||||
const $query = $('#sql-console-query');
 | 
			
		||||
const $executeButton = $('#sql-console-execute');
 | 
			
		||||
const $resultHead = $('#sql-console-results thead');
 | 
			
		||||
const $resultBody = $('#sql-console-results tbody');
 | 
			
		||||
 | 
			
		||||
    let codeEditor;
 | 
			
		||||
let codeEditor;
 | 
			
		||||
 | 
			
		||||
    function showDialog() {
 | 
			
		||||
function showDialog() {
 | 
			
		||||
    glob.activeDialog = $dialog;
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog({
 | 
			
		||||
@@ -20,15 +22,18 @@ const sqlConsole = (function() {
 | 
			
		||||
            initEditor();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    async function initEditor() {
 | 
			
		||||
async function initEditor() {
 | 
			
		||||
    if (!codeEditor) {
 | 
			
		||||
            await requireLibrary(CODE_MIRROR);
 | 
			
		||||
        await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
 | 
			
		||||
 | 
			
		||||
        CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
 | 
			
		||||
        CodeMirror.keyMap.default["Tab"] = "indentMore";
 | 
			
		||||
 | 
			
		||||
        // removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
 | 
			
		||||
        delete CodeMirror.keyMap.basic["Esc"];
 | 
			
		||||
 | 
			
		||||
        CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 | 
			
		||||
 | 
			
		||||
        codeEditor = CodeMirror($query[0], {
 | 
			
		||||
@@ -43,9 +48,13 @@ const sqlConsole = (function() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    codeEditor.focus();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function execute(e) {
 | 
			
		||||
    // stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
    async function execute() {
 | 
			
		||||
    const sqlQuery = codeEditor.getValue();
 | 
			
		||||
 | 
			
		||||
    const result = await server.post("sql/execute", {
 | 
			
		||||
@@ -53,11 +62,11 @@ const sqlConsole = (function() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!result.success) {
 | 
			
		||||
            showError(result.error);
 | 
			
		||||
        infoService.showError(result.error);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
            showMessage("Query was executed successfully.");
 | 
			
		||||
        infoService.showMessage("Query was executed successfully.");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const rows = result.rows;
 | 
			
		||||
@@ -85,15 +94,12 @@ const sqlConsole = (function() {
 | 
			
		||||
 | 
			
		||||
        $resultBody.append(rowEl);
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'alt+o', showDialog);
 | 
			
		||||
$query.bind('keydown', 'ctrl+return', execute);
 | 
			
		||||
 | 
			
		||||
    $query.bind('keydown', 'ctrl+return', execute);
 | 
			
		||||
$executeButton.click(execute);
 | 
			
		||||
 | 
			
		||||
    $executeButton.click(execute);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
export default {
 | 
			
		||||
    showDialog
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										26
									
								
								src/public/javascripts/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/public/javascripts/entities/branch.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
class Branch {
 | 
			
		||||
    constructor(treeCache, row) {
 | 
			
		||||
        this.treeCache = treeCache;
 | 
			
		||||
        this.branchId = row.branchId;
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        this.note = null;
 | 
			
		||||
        this.parentNoteId = row.parentNoteId;
 | 
			
		||||
        this.notePosition = row.notePosition;
 | 
			
		||||
        this.prefix = row.prefix;
 | 
			
		||||
        this.isExpanded = row.isExpanded;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await this.treeCache.getNote(this.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isTopLevel() {
 | 
			
		||||
        return this.parentNoteId === 'root';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get toString() {
 | 
			
		||||
        return `Branch(branchId=${this.branchId})`;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Branch;
 | 
			
		||||
							
								
								
									
										15
									
								
								src/public/javascripts/entities/note_full.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/public/javascripts/entities/note_full.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import NoteShort from './note_short.js';
 | 
			
		||||
 | 
			
		||||
class NoteFull extends NoteShort {
 | 
			
		||||
    constructor(treeCache, row) {
 | 
			
		||||
        super(treeCache, row);
 | 
			
		||||
 | 
			
		||||
        this.content = row.content;
 | 
			
		||||
 | 
			
		||||
        if (this.isJson()) {
 | 
			
		||||
            this.jsonContent = JSON.parse(this.content);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NoteFull;
 | 
			
		||||
							
								
								
									
										57
									
								
								src/public/javascripts/entities/note_short.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/public/javascripts/entities/note_short.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
class NoteShort {
 | 
			
		||||
    constructor(treeCache, row) {
 | 
			
		||||
        this.treeCache = treeCache;
 | 
			
		||||
        this.noteId = row.noteId;
 | 
			
		||||
        this.title = row.title;
 | 
			
		||||
        this.isProtected = row.isProtected;
 | 
			
		||||
        this.type = row.type;
 | 
			
		||||
        this.mime = row.mime;
 | 
			
		||||
        this.hideInAutocomplete = row.hideInAutocomplete;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isJson() {
 | 
			
		||||
        return this.mime === "application/json";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBranches() {
 | 
			
		||||
        const branches = [];
 | 
			
		||||
 | 
			
		||||
        for (const parent of this.treeCache.parents[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildBranches() {
 | 
			
		||||
        const branches = [];
 | 
			
		||||
 | 
			
		||||
        for (const child of this.treeCache.children[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
        return this.treeCache.parents[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
        return this.treeCache.children[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get toString() {
 | 
			
		||||
        return `Note(noteId=${this.noteId}, title=${this.title})`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get dto() {
 | 
			
		||||
        const dto = Object.assign({}, this);
 | 
			
		||||
        delete dto.treeCache;
 | 
			
		||||
        delete dto.hideInAutocomplete;
 | 
			
		||||
 | 
			
		||||
        return dto;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NoteShort;
 | 
			
		||||
@@ -1,238 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
// hot keys are active also inside inputs and content editables
 | 
			
		||||
jQuery.hotkeys.options.filterInputAcceptingElements = false;
 | 
			
		||||
jQuery.hotkeys.options.filterContentEditable = false;
 | 
			
		||||
jQuery.hotkeys.options.filterTextInputs = false;
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'alt+m', e => {
 | 
			
		||||
    $(".hide-toggle").toggleClass("suppressed");
 | 
			
		||||
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// hide (toggle) everything except for the note content for distraction free writing
 | 
			
		||||
$(document).bind('keydown', 'alt+t', e => {
 | 
			
		||||
    const date = new Date();
 | 
			
		||||
    const dateString = formatDateTime(date);
 | 
			
		||||
 | 
			
		||||
    link.addTextToEditor(dateString);
 | 
			
		||||
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'f5', () => {
 | 
			
		||||
    reloadApp();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'ctrl+r', () => {
 | 
			
		||||
    reloadApp();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        require('electron').remote.getCurrentWindow().toggleDevTools();
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'ctrl+f', () => {
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        const searchInPage = require('electron-in-page-search').default;
 | 
			
		||||
        const remote = require('electron').remote;
 | 
			
		||||
 | 
			
		||||
        const inPageSearch = searchInPage(remote.getCurrentWebContents());
 | 
			
		||||
 | 
			
		||||
        inPageSearch.openSearchWindow();
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', "ctrl+shift+up", () => {
 | 
			
		||||
    const node = noteTree.getCurrentNode();
 | 
			
		||||
    node.navigate($.ui.keyCode.UP, true);
 | 
			
		||||
 | 
			
		||||
    $("#note-detail").focus();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', "ctrl+shift+down", () => {
 | 
			
		||||
    const node = noteTree.getCurrentNode();
 | 
			
		||||
    node.navigate($.ui.keyCode.DOWN, true);
 | 
			
		||||
 | 
			
		||||
    $("#note-detail").focus();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'ctrl+-', () => {
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
        if (webFrame.getZoomFactor() > 0.2) {
 | 
			
		||||
            webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', 'ctrl+=', () => {
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
        webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#note-title").bind('keydown', 'return', () => $("#note-detail").focus());
 | 
			
		||||
 | 
			
		||||
$(window).on('beforeunload', () => {
 | 
			
		||||
    // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
 | 
			
		||||
    // this sends the request asynchronously and doesn't wait for result
 | 
			
		||||
    noteEditor.saveNoteIfChanged();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
 | 
			
		||||
$.ui.autocomplete.filter = (array, terms) => {
 | 
			
		||||
    if (!terms) {
 | 
			
		||||
        return array;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const startDate = new Date();
 | 
			
		||||
 | 
			
		||||
    const results = [];
 | 
			
		||||
    const tokens = terms.toLowerCase().split(" ");
 | 
			
		||||
 | 
			
		||||
    for (const item of array) {
 | 
			
		||||
        let found = true;
 | 
			
		||||
        const lcLabel = item.label.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        for (const token of tokens) {
 | 
			
		||||
            if (lcLabel.indexOf(token) === -1) {
 | 
			
		||||
                found = false;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (found) {
 | 
			
		||||
            results.push(item);
 | 
			
		||||
 | 
			
		||||
            if (results.length > 100) {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
$(document).tooltip({
 | 
			
		||||
    items: "#note-detail a",
 | 
			
		||||
    content: function(callback) {
 | 
			
		||||
        const notePath = link.getNotePathFromLink($(this).attr("href"));
 | 
			
		||||
 | 
			
		||||
        if (notePath !== null) {
 | 
			
		||||
            const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
            noteEditor.loadNote(noteId).then(note => callback(note.detail.content));
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    close: function(event, ui)
 | 
			
		||||
    {
 | 
			
		||||
        ui.tooltip.hover(function()
 | 
			
		||||
        {
 | 
			
		||||
            $(this).stop(true).fadeTo(400, 1);
 | 
			
		||||
        },
 | 
			
		||||
        function()
 | 
			
		||||
        {
 | 
			
		||||
            $(this).fadeOut('400', function()
 | 
			
		||||
            {
 | 
			
		||||
                $(this).remove();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
			
		||||
    const string = msg.toLowerCase();
 | 
			
		||||
 | 
			
		||||
    let message = "Uncaught error: ";
 | 
			
		||||
 | 
			
		||||
    if (string.indexOf("script error") > -1){
 | 
			
		||||
        message += 'No details available';
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        message += [
 | 
			
		||||
            'Message: ' + msg,
 | 
			
		||||
            'URL: ' + url,
 | 
			
		||||
            'Line: ' + lineNo,
 | 
			
		||||
            'Column: ' + columnNo,
 | 
			
		||||
            'Error object: ' + JSON.stringify(error)
 | 
			
		||||
        ].join(' - ');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    messaging.logError(message);
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
$("#logout-button").toggle(!isElectron());
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
    server.get("script/startup").then(scripts => {
 | 
			
		||||
        for (const script of scripts) {
 | 
			
		||||
            executeScript(script);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if (isElectron()) {
 | 
			
		||||
    require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
 | 
			
		||||
        // this might occur when day note had to be created
 | 
			
		||||
        if (!noteTree.noteExists(parentNoteId)) {
 | 
			
		||||
            await noteTree.reload();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await noteTree.activateNode(parentNoteId);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            const node = noteTree.getCurrentNode();
 | 
			
		||||
 | 
			
		||||
            noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
 | 
			
		||||
        }, 500);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function uploadAttachment() {
 | 
			
		||||
    $("#file-upload").trigger('click');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$("#file-upload").change(async function() {
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('upload', this.files[0]);
 | 
			
		||||
 | 
			
		||||
    const resp = await $.ajax({
 | 
			
		||||
        url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
 | 
			
		||||
        headers: server.getHeaders(),
 | 
			
		||||
        data: formData,
 | 
			
		||||
        type: 'POST',
 | 
			
		||||
        contentType: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
        processData: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await noteTree.reload();
 | 
			
		||||
 | 
			
		||||
    await noteTree.activateNode(resp.noteId);
 | 
			
		||||
});
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const link = (function() {
 | 
			
		||||
    function getNotePathFromLink(url) {
 | 
			
		||||
        const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
 | 
			
		||||
 | 
			
		||||
        if (notePathMatch === null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            return notePathMatch[1];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNodePathFromLabel(label) {
 | 
			
		||||
        const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
 | 
			
		||||
 | 
			
		||||
        if (notePathMatch !== null) {
 | 
			
		||||
            return notePathMatch[1];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
        if (!noteTitle) {
 | 
			
		||||
            const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
            noteTitle = noteTree.getNoteTitle(noteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const noteLink = $("<a>", {
 | 
			
		||||
            href: 'javascript:',
 | 
			
		||||
            text: noteTitle
 | 
			
		||||
        }).attr('action', 'note')
 | 
			
		||||
            .attr('note-path', notePath);
 | 
			
		||||
 | 
			
		||||
        return noteLink;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function goToLink(e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const $link = $(e.target);
 | 
			
		||||
        let notePath = $link.attr("note-path");
 | 
			
		||||
 | 
			
		||||
        if (!notePath) {
 | 
			
		||||
            const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
 | 
			
		||||
 | 
			
		||||
            if (!address) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (address.startsWith('http')) {
 | 
			
		||||
                window.open(address, '_blank');
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            notePath = getNotePathFromLink(address);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        noteTree.activateNode(notePath);
 | 
			
		||||
 | 
			
		||||
        // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
 | 
			
		||||
        $("[role='tooltip']").remove();
 | 
			
		||||
 | 
			
		||||
        if (glob.activeDialog) {
 | 
			
		||||
            try {
 | 
			
		||||
                glob.activeDialog.dialog('close');
 | 
			
		||||
            }
 | 
			
		||||
            catch (e) {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addLinkToEditor(linkTitle, linkHref) {
 | 
			
		||||
        const editor = noteEditor.getEditor();
 | 
			
		||||
        const doc = editor.document;
 | 
			
		||||
 | 
			
		||||
        doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addTextToEditor(text) {
 | 
			
		||||
        const editor = noteEditor.getEditor();
 | 
			
		||||
        const doc = editor.document;
 | 
			
		||||
 | 
			
		||||
        doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
 | 
			
		||||
    // of opening the link in new window/tab
 | 
			
		||||
    $(document).on('click', "a[action='note']", goToLink);
 | 
			
		||||
    $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
 | 
			
		||||
    $(document).on('dblclick', '#note-detail a', goToLink);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        getNodePathFromLabel,
 | 
			
		||||
        getNotePathFromLink,
 | 
			
		||||
        createNoteLink,
 | 
			
		||||
        addLinkToEditor,
 | 
			
		||||
        addTextToEditor
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,115 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const messaging = (function() {
 | 
			
		||||
    const $changesToPushCount = $("#changes-to-push-count");
 | 
			
		||||
 | 
			
		||||
    function logError(message) {
 | 
			
		||||
        console.log(now(), message); // needs to be separate from .trace()
 | 
			
		||||
        console.trace();
 | 
			
		||||
 | 
			
		||||
        if (ws && ws.readyState === 1) {
 | 
			
		||||
            ws.send(JSON.stringify({
 | 
			
		||||
                type: 'log-error',
 | 
			
		||||
                error: message
 | 
			
		||||
            }));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function messageHandler(event) {
 | 
			
		||||
        const message = JSON.parse(event.data);
 | 
			
		||||
 | 
			
		||||
        if (message.type === 'sync') {
 | 
			
		||||
            lastPingTs = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
            if (message.data.length > 0) {
 | 
			
		||||
                console.log(now(), "Sync data: ", message.data);
 | 
			
		||||
 | 
			
		||||
                lastSyncId = message.data[message.data.length - 1].id;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
 | 
			
		||||
 | 
			
		||||
            if (syncData.some(sync => sync.entityName === 'note_tree')
 | 
			
		||||
                || syncData.some(sync => sync.entityName === 'notes')) {
 | 
			
		||||
 | 
			
		||||
                console.log(now(), "Reloading tree because of background changes");
 | 
			
		||||
 | 
			
		||||
                noteTree.reload();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === noteEditor.getCurrentNoteId())) {
 | 
			
		||||
                showMessage('Reloading note because of background changes');
 | 
			
		||||
 | 
			
		||||
                noteEditor.reload();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (syncData.some(sync => sync.entityName === 'recent_notes')) {
 | 
			
		||||
                console.log(now(), "Reloading recent notes because of background changes");
 | 
			
		||||
 | 
			
		||||
                recentNotes.reload();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // we don't detect image changes here since images themselves are immutable and references should be
 | 
			
		||||
            // updated in note detail as well
 | 
			
		||||
 | 
			
		||||
            $changesToPushCount.html(message.changesToPushCount);
 | 
			
		||||
        }
 | 
			
		||||
        else if (message.type === 'sync-hash-check-failed') {
 | 
			
		||||
            showError("Sync check failed!", 60000);
 | 
			
		||||
        }
 | 
			
		||||
        else if (message.type === 'consistency-checks-failed') {
 | 
			
		||||
            showError("Consistency checks failed! See logs for details.", 50 * 60000);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function connectWebSocket() {
 | 
			
		||||
        const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
 | 
			
		||||
 | 
			
		||||
        // use wss for secure messaging
 | 
			
		||||
        const ws = new WebSocket(protocol + "://" + location.host);
 | 
			
		||||
        ws.onopen = event => console.log(now(), "Connected to server with WebSocket");
 | 
			
		||||
        ws.onmessage = messageHandler;
 | 
			
		||||
        ws.onclose = function(){
 | 
			
		||||
            // Try to reconnect in 5 seconds
 | 
			
		||||
            setTimeout(() => connectWebSocket(), 5000);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return ws;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ws = connectWebSocket();
 | 
			
		||||
 | 
			
		||||
    let lastSyncId = glob.maxSyncIdAtLoad;
 | 
			
		||||
    let lastPingTs = new Date().getTime();
 | 
			
		||||
    let connectionBrokenNotification = null;
 | 
			
		||||
 | 
			
		||||
    setInterval(async () => {
 | 
			
		||||
        if (new Date().getTime() - lastPingTs > 5000) {
 | 
			
		||||
            if (!connectionBrokenNotification) {
 | 
			
		||||
                connectionBrokenNotification = $.notify({
 | 
			
		||||
                    // options
 | 
			
		||||
                    message: "Lost connection to server"
 | 
			
		||||
                },{
 | 
			
		||||
                    // settings
 | 
			
		||||
                    type: 'danger',
 | 
			
		||||
                    delay: 100000000 // keep it until we explicitly close it
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (connectionBrokenNotification) {
 | 
			
		||||
            await connectionBrokenNotification.close();
 | 
			
		||||
            connectionBrokenNotification = null;
 | 
			
		||||
 | 
			
		||||
            showMessage("Re-connected to server");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ws.send(JSON.stringify({
 | 
			
		||||
            type: 'ping',
 | 
			
		||||
            lastSyncId: lastSyncId
 | 
			
		||||
        }));
 | 
			
		||||
    }, 1000);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        logError
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import server from './services/server.js';
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
    server.get('migration').then(result => {
 | 
			
		||||
        const appDbVersion = result.app_db_version;
 | 
			
		||||
        const dbVersion = result.db_version;
 | 
			
		||||
$(document).ready(async () => {
 | 
			
		||||
    const {appDbVersion, dbVersion} = await server.get('migration');
 | 
			
		||||
 | 
			
		||||
    console.log("HI", {appDbVersion, dbVersion});
 | 
			
		||||
 | 
			
		||||
    if (appDbVersion === dbVersion) {
 | 
			
		||||
        $("#up-to-date").show();
 | 
			
		||||
@@ -14,7 +14,6 @@ $(document).ready(() => {
 | 
			
		||||
        $("#app-db-version").html(appDbVersion);
 | 
			
		||||
        $("#db-version").html(dbVersion);
 | 
			
		||||
    }
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$("#run-migration").click(async () => {
 | 
			
		||||
@@ -26,7 +25,7 @@ $("#run-migration").click(async () => {
 | 
			
		||||
 | 
			
		||||
    for (const migration of result.migrations) {
 | 
			
		||||
        const row = $('<tr>')
 | 
			
		||||
            .append($('<td>').html(migration.db_version))
 | 
			
		||||
            .append($('<td>').html(migration.dbVersion))
 | 
			
		||||
            .append($('<td>').html(migration.name))
 | 
			
		||||
            .append($('<td>').html(migration.success ? 'Yes' : 'No'))
 | 
			
		||||
            .append($('<td>').html(migration.success ? 'N/A' : migration.error));
 | 
			
		||||
@@ -38,3 +37,10 @@ $("#run-migration").click(async () => {
 | 
			
		||||
        $("#migration-table").append(row);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// copy of this shortcut to be able to debug migration problems
 | 
			
		||||
$(document).bind('keydown', 'ctrl+shift+i', () => {
 | 
			
		||||
    require('electron').remote.getCurrentWindow().toggleDevTools();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
@@ -1,369 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteEditor = (function() {
 | 
			
		||||
    const $noteTitle = $("#note-title");
 | 
			
		||||
 | 
			
		||||
    const $noteDetail = $('#note-detail');
 | 
			
		||||
    const $noteDetailCode = $('#note-detail-code');
 | 
			
		||||
    const $noteDetailRender = $('#note-detail-render');
 | 
			
		||||
    const $noteDetailAttachment = $('#note-detail-attachment');
 | 
			
		||||
 | 
			
		||||
    const $protectButton = $("#protect-button");
 | 
			
		||||
    const $unprotectButton = $("#unprotect-button");
 | 
			
		||||
    const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
			
		||||
    const $noteIdDisplay = $("#note-id-display");
 | 
			
		||||
    const $attributeList = $("#attribute-list");
 | 
			
		||||
    const $attributeListInner = $("#attribute-list-inner");
 | 
			
		||||
    const $attachmentFileName = $("#attachment-filename");
 | 
			
		||||
    const $attachmentFileType = $("#attachment-filetype");
 | 
			
		||||
    const $attachmentFileSize = $("#attachment-filesize");
 | 
			
		||||
    const $attachmentDownload = $("#attachment-download");
 | 
			
		||||
    const $attachmentOpen = $("#attachment-open");
 | 
			
		||||
 | 
			
		||||
    let editor = null;
 | 
			
		||||
    let codeEditor = null;
 | 
			
		||||
 | 
			
		||||
    let currentNote = null;
 | 
			
		||||
 | 
			
		||||
    let noteChangeDisabled = false;
 | 
			
		||||
 | 
			
		||||
    let isNoteChanged = false;
 | 
			
		||||
 | 
			
		||||
    function getCurrentNote() {
 | 
			
		||||
        return currentNote;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getCurrentNoteId() {
 | 
			
		||||
        return currentNote ? currentNote.detail.noteId : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function noteChanged() {
 | 
			
		||||
        if (noteChangeDisabled) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        isNoteChanged = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function reload() {
 | 
			
		||||
        // no saving here
 | 
			
		||||
 | 
			
		||||
        await loadNoteToEditor(getCurrentNoteId());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function switchToNote(noteId) {
 | 
			
		||||
        if (getCurrentNoteId() !== noteId) {
 | 
			
		||||
            await saveNoteIfChanged();
 | 
			
		||||
 | 
			
		||||
            await loadNoteToEditor(noteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function saveNoteIfChanged() {
 | 
			
		||||
        if (!isNoteChanged) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const note = noteEditor.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        updateNoteFromInputs(note);
 | 
			
		||||
 | 
			
		||||
        await saveNoteToServer(note);
 | 
			
		||||
 | 
			
		||||
        if (note.detail.isProtected) {
 | 
			
		||||
            protected_session.touchProtectedSession();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateNoteFromInputs(note) {
 | 
			
		||||
        if (note.detail.type === 'text') {
 | 
			
		||||
            note.detail.content = editor.getData();
 | 
			
		||||
 | 
			
		||||
            // if content is only tags/whitespace (typically <p> </p>), then just make it empty
 | 
			
		||||
            // this is important when setting new note to code
 | 
			
		||||
            if (jQuery(note.detail.content).text().trim() === '') {
 | 
			
		||||
                note.detail.content = ''
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (note.detail.type === 'code') {
 | 
			
		||||
            note.detail.content = codeEditor.getValue();
 | 
			
		||||
        }
 | 
			
		||||
        else if (note.detail.type === 'render' || note.detail.type === 'file') {
 | 
			
		||||
            // nothing
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throwError("Unrecognized type: " + note.detail.type);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const title = $noteTitle.val();
 | 
			
		||||
 | 
			
		||||
        note.detail.title = title;
 | 
			
		||||
 | 
			
		||||
        noteTree.setNoteTitle(note.detail.noteId, title);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function saveNoteToServer(note) {
 | 
			
		||||
        await server.put('notes/' + note.detail.noteId, note);
 | 
			
		||||
 | 
			
		||||
        isNoteChanged = false;
 | 
			
		||||
 | 
			
		||||
        showMessage("Saved!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setNoteBackgroundIfProtected(note) {
 | 
			
		||||
        const isProtected = !!note.detail.isProtected;
 | 
			
		||||
 | 
			
		||||
        $noteDetailWrapper.toggleClass("protected", isProtected);
 | 
			
		||||
        $protectButton.toggle(!isProtected);
 | 
			
		||||
        $unprotectButton.toggle(isProtected);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let isNewNoteCreated = false;
 | 
			
		||||
 | 
			
		||||
    function newNoteCreated() {
 | 
			
		||||
        isNewNoteCreated = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function setContent(content) {
 | 
			
		||||
        if (currentNote.detail.type === 'text') {
 | 
			
		||||
            if (!editor) {
 | 
			
		||||
                await requireLibrary(CKEDITOR);
 | 
			
		||||
 | 
			
		||||
                editor = await BalloonEditor.create($noteDetail[0], {});
 | 
			
		||||
 | 
			
		||||
                editor.document.on('change', noteChanged);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
 | 
			
		||||
            editor.setData(content ? content : "<p></p>");
 | 
			
		||||
 | 
			
		||||
            $noteDetail.show();
 | 
			
		||||
        }
 | 
			
		||||
        else if (currentNote.detail.type === 'code') {
 | 
			
		||||
            if (!codeEditor) {
 | 
			
		||||
                await requireLibrary(CODE_MIRROR);
 | 
			
		||||
 | 
			
		||||
                CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
 | 
			
		||||
                CodeMirror.keyMap.default["Tab"] = "indentMore";
 | 
			
		||||
 | 
			
		||||
                CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 | 
			
		||||
 | 
			
		||||
                codeEditor = CodeMirror($("#note-detail-code")[0], {
 | 
			
		||||
                    value: "",
 | 
			
		||||
                    viewportMargin: Infinity,
 | 
			
		||||
                    indentUnit: 4,
 | 
			
		||||
                    matchBrackets: true,
 | 
			
		||||
                    matchTags: { bothTags: true },
 | 
			
		||||
                    highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                codeEditor.on('change', noteChanged);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $noteDetailCode.show();
 | 
			
		||||
 | 
			
		||||
            // this needs to happen after the element is shown, otherwise the editor won't be refresheds
 | 
			
		||||
            codeEditor.setValue(content);
 | 
			
		||||
 | 
			
		||||
            const info = CodeMirror.findModeByMIME(currentNote.detail.mime);
 | 
			
		||||
 | 
			
		||||
            if (info) {
 | 
			
		||||
                codeEditor.setOption("mode", info.mime);
 | 
			
		||||
                CodeMirror.autoLoadMode(codeEditor, info.mode);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function loadNoteToEditor(noteId) {
 | 
			
		||||
        currentNote = await loadNote(noteId);
 | 
			
		||||
 | 
			
		||||
        if (isNewNoteCreated) {
 | 
			
		||||
            isNewNoteCreated = false;
 | 
			
		||||
 | 
			
		||||
            $noteTitle.focus().select();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $noteIdDisplay.html(noteId);
 | 
			
		||||
 | 
			
		||||
        await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
 | 
			
		||||
 | 
			
		||||
        if (currentNote.detail.isProtected) {
 | 
			
		||||
            protected_session.touchProtectedSession();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this might be important if we focused on protected note when not in protected note and we got a dialog
 | 
			
		||||
        // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
 | 
			
		||||
        protected_session.ensureDialogIsClosed();
 | 
			
		||||
 | 
			
		||||
        $noteDetailWrapper.show();
 | 
			
		||||
 | 
			
		||||
        noteChangeDisabled = true;
 | 
			
		||||
 | 
			
		||||
        $noteTitle.val(currentNote.detail.title);
 | 
			
		||||
 | 
			
		||||
        noteType.setNoteType(currentNote.detail.type);
 | 
			
		||||
        noteType.setNoteMime(currentNote.detail.mime);
 | 
			
		||||
 | 
			
		||||
        $noteDetail.hide();
 | 
			
		||||
        $noteDetailCode.hide();
 | 
			
		||||
        $noteDetailRender.html('').hide();
 | 
			
		||||
        $noteDetailAttachment.hide();
 | 
			
		||||
 | 
			
		||||
        if (currentNote.detail.type === 'render') {
 | 
			
		||||
            $noteDetailRender.show();
 | 
			
		||||
 | 
			
		||||
            const subTree = await server.get('script/subtree/' + getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
            $noteDetailRender.html(subTree);
 | 
			
		||||
        }
 | 
			
		||||
        else if (currentNote.detail.type === 'file') {
 | 
			
		||||
            $noteDetailAttachment.show();
 | 
			
		||||
 | 
			
		||||
            $attachmentFileName.text(currentNote.attributes.original_file_name);
 | 
			
		||||
            $attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
 | 
			
		||||
            $attachmentFileType.text(currentNote.detail.mime);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            await setContent(currentNote.detail.content);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        noteChangeDisabled = false;
 | 
			
		||||
 | 
			
		||||
        setNoteBackgroundIfProtected(currentNote);
 | 
			
		||||
        noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
 | 
			
		||||
 | 
			
		||||
        // after loading new note make sure editor is scrolled to the top
 | 
			
		||||
        $noteDetailWrapper.scrollTop(0);
 | 
			
		||||
 | 
			
		||||
        loadAttributeList();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function loadAttributeList() {
 | 
			
		||||
        const noteId = getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
        const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
			
		||||
 | 
			
		||||
        $attributeListInner.html('');
 | 
			
		||||
 | 
			
		||||
        if (attributes.length > 0) {
 | 
			
		||||
            for (const attr of attributes) {
 | 
			
		||||
                $attributeListInner.append(formatAttribute(attr) + " ");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $attributeList.show();
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $attributeList.hide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function loadNote(noteId) {
 | 
			
		||||
        return await server.get('notes/' + noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getEditor() {
 | 
			
		||||
        return editor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function focus() {
 | 
			
		||||
        const note = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        if (note.detail.type === 'text') {
 | 
			
		||||
            $noteDetail.focus();
 | 
			
		||||
        }
 | 
			
		||||
        else if (note.detail.type === 'code') {
 | 
			
		||||
            codeEditor.focus();
 | 
			
		||||
        }
 | 
			
		||||
        else if (note.detail.type === 'render' || note.detail.type === 'file') {
 | 
			
		||||
            // do nothing
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throwError('Unrecognized type: ' + note.detail.type);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getCurrentNoteType() {
 | 
			
		||||
        const currentNote = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        return currentNote ? currentNote.detail.type : null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function executeCurrentNote() {
 | 
			
		||||
        if (getCurrentNoteType() === 'code') {
 | 
			
		||||
            // make sure note is saved so we load latest changes
 | 
			
		||||
            await saveNoteIfChanged();
 | 
			
		||||
 | 
			
		||||
            const script = await server.get('script/subtree/' + getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
            executeScript(script);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $attachmentDownload.click(() => {
 | 
			
		||||
        if (isElectron()) {
 | 
			
		||||
            const remote = require('electron').remote;
 | 
			
		||||
 | 
			
		||||
            remote.getCurrentWebContents().downloadURL(getAttachmentUrl());
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            window.location.href = getAttachmentUrl();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $attachmentOpen.click(() => {
 | 
			
		||||
        if (isElectron()) {
 | 
			
		||||
            const open = require("open");
 | 
			
		||||
 | 
			
		||||
            open(getAttachmentUrl());
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            window.location.href = getAttachmentUrl();
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function getAttachmentUrl() {
 | 
			
		||||
        // electron needs absolute URL so we extract current host, port, protocol
 | 
			
		||||
        const url = new URL(window.location.href);
 | 
			
		||||
        const host = url.protocol + "//" + url.hostname + ":" + url.port;
 | 
			
		||||
 | 
			
		||||
        const downloadUrl = "/api/attachments/download/" + getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
        return host + downloadUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).ready(() => {
 | 
			
		||||
        $noteTitle.on('input', () => {
 | 
			
		||||
            noteChanged();
 | 
			
		||||
 | 
			
		||||
            const title = $noteTitle.val();
 | 
			
		||||
 | 
			
		||||
            noteTree.setNoteTitle(getCurrentNoteId(), title);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // so that tab jumps from note title (which has tabindex 1)
 | 
			
		||||
        $noteDetail.attr("tabindex", 2);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', "ctrl+return", executeCurrentNote);
 | 
			
		||||
 | 
			
		||||
    setInterval(saveNoteIfChanged, 5000);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        reload,
 | 
			
		||||
        switchToNote,
 | 
			
		||||
        saveNoteIfChanged,
 | 
			
		||||
        updateNoteFromInputs,
 | 
			
		||||
        saveNoteToServer,
 | 
			
		||||
        setNoteBackgroundIfProtected,
 | 
			
		||||
        loadNote,
 | 
			
		||||
        getCurrentNote,
 | 
			
		||||
        getCurrentNoteType,
 | 
			
		||||
        getCurrentNoteId,
 | 
			
		||||
        newNoteCreated,
 | 
			
		||||
        getEditor,
 | 
			
		||||
        focus,
 | 
			
		||||
        executeCurrentNote,
 | 
			
		||||
        loadAttributeList,
 | 
			
		||||
        setContent
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,899 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteTree = (function() {
 | 
			
		||||
    const $tree = $("#tree");
 | 
			
		||||
    const $parentList = $("#parent-list");
 | 
			
		||||
    const $parentListList = $("#parent-list-inner");
 | 
			
		||||
 | 
			
		||||
    let startNotePath = null;
 | 
			
		||||
    let notesTreeMap = {};
 | 
			
		||||
 | 
			
		||||
    let parentToChildren = {};
 | 
			
		||||
    let childToParents = {};
 | 
			
		||||
 | 
			
		||||
    let parentChildToNoteTreeId = {};
 | 
			
		||||
    let noteIdToTitle = {};
 | 
			
		||||
 | 
			
		||||
    let hiddenInAutocomplete = {};
 | 
			
		||||
 | 
			
		||||
    function getNoteTreeId(parentNoteId, childNoteId) {
 | 
			
		||||
        assertArguments(parentNoteId, childNoteId);
 | 
			
		||||
 | 
			
		||||
        const key = parentNoteId + "-" + childNoteId;
 | 
			
		||||
 | 
			
		||||
        // this can return undefined and client code should deal with it somehow
 | 
			
		||||
 | 
			
		||||
        return parentChildToNoteTreeId[key];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNoteTitle(noteId, parentNoteId = null) {
 | 
			
		||||
        assertArguments(noteId);
 | 
			
		||||
 | 
			
		||||
        let title = noteIdToTitle[noteId];
 | 
			
		||||
 | 
			
		||||
        if (!title) {
 | 
			
		||||
            throwError("Can't find title for noteId='" + noteId + "'");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (parentNoteId !== null) {
 | 
			
		||||
            const noteTreeId = getNoteTreeId(parentNoteId, noteId);
 | 
			
		||||
 | 
			
		||||
            if (noteTreeId) {
 | 
			
		||||
                const noteTree = notesTreeMap[noteTreeId];
 | 
			
		||||
 | 
			
		||||
                if (noteTree.prefix) {
 | 
			
		||||
                    title = noteTree.prefix + ' - ' + title;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return title;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // note that if you want to access data like noteId or isProtected, you need to go into "data" property
 | 
			
		||||
    function getCurrentNode() {
 | 
			
		||||
        return $tree.fancytree("getActiveNode");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getCurrentNotePath() {
 | 
			
		||||
        const node = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        return treeUtils.getNotePath(node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNodesByNoteTreeId(noteTreeId) {
 | 
			
		||||
        assertArguments(noteTreeId);
 | 
			
		||||
 | 
			
		||||
        const noteTree = notesTreeMap[noteTreeId];
 | 
			
		||||
 | 
			
		||||
        return getNodesByNoteId(noteTree.noteId).filter(node => node.data.noteTreeId === noteTreeId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNodesByNoteId(noteId) {
 | 
			
		||||
        assertArguments(noteId);
 | 
			
		||||
 | 
			
		||||
        const list = getTree().getNodesByRef(noteId);
 | 
			
		||||
        return list ? list : []; // if no nodes with this refKey are found, fancy tree returns null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setPrefix(noteTreeId, prefix) {
 | 
			
		||||
        assertArguments(noteTreeId);
 | 
			
		||||
 | 
			
		||||
        notesTreeMap[noteTreeId].prefix = prefix;
 | 
			
		||||
 | 
			
		||||
        getNodesByNoteTreeId(noteTreeId).map(node => {
 | 
			
		||||
            node.data.prefix = prefix;
 | 
			
		||||
 | 
			
		||||
            treeUtils.setNodeTitleWithPrefix(node);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function removeParentChildRelation(parentNoteId, childNoteId) {
 | 
			
		||||
        assertArguments(parentNoteId, childNoteId);
 | 
			
		||||
 | 
			
		||||
        const key = parentNoteId + "-" + childNoteId;
 | 
			
		||||
 | 
			
		||||
        delete parentChildToNoteTreeId[key];
 | 
			
		||||
 | 
			
		||||
        parentToChildren[parentNoteId] = parentToChildren[parentNoteId].filter(noteId => noteId !== childNoteId);
 | 
			
		||||
        childToParents[childNoteId] = childToParents[childNoteId].filter(noteId => noteId !== parentNoteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setParentChildRelation(noteTreeId, parentNoteId, childNoteId) {
 | 
			
		||||
        assertArguments(noteTreeId, parentNoteId, childNoteId);
 | 
			
		||||
 | 
			
		||||
        const key = parentNoteId + "-" + childNoteId;
 | 
			
		||||
 | 
			
		||||
        parentChildToNoteTreeId[key] = noteTreeId;
 | 
			
		||||
 | 
			
		||||
        if (!parentToChildren[parentNoteId]) {
 | 
			
		||||
            parentToChildren[parentNoteId] = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        parentToChildren[parentNoteId].push(childNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!childToParents[childNoteId]) {
 | 
			
		||||
            childToParents[childNoteId] = [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        childToParents[childNoteId].push(parentNoteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareNoteTree(notes) {
 | 
			
		||||
        assertArguments(notes);
 | 
			
		||||
 | 
			
		||||
        parentToChildren = {};
 | 
			
		||||
        childToParents = {};
 | 
			
		||||
        notesTreeMap = {};
 | 
			
		||||
 | 
			
		||||
        for (const note of notes) {
 | 
			
		||||
            notesTreeMap[note.noteTreeId] = note;
 | 
			
		||||
 | 
			
		||||
            noteIdToTitle[note.noteId] = note.title;
 | 
			
		||||
 | 
			
		||||
            delete note.title; // this should not be used. Use noteIdToTitle instead
 | 
			
		||||
 | 
			
		||||
            setParentChildRelation(note.noteTreeId, note.parentNoteId, note.noteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return prepareNoteTreeInner('root');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getExtraClasses(note) {
 | 
			
		||||
        assertArguments(note);
 | 
			
		||||
 | 
			
		||||
        const extraClasses = [];
 | 
			
		||||
 | 
			
		||||
        if (note.isProtected) {
 | 
			
		||||
            extraClasses.push("protected");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (childToParents[note.noteId].length > 1) {
 | 
			
		||||
            extraClasses.push("multiple-parents");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (note.type === 'code') {
 | 
			
		||||
            extraClasses.push("code");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return extraClasses.join(" ");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareNoteTreeInner(parentNoteId) {
 | 
			
		||||
        assertArguments(parentNoteId);
 | 
			
		||||
 | 
			
		||||
        const childNoteIds = parentToChildren[parentNoteId];
 | 
			
		||||
        if (!childNoteIds) {
 | 
			
		||||
            messaging.logError("No children for " + parentNoteId + ". This shouldn't happen.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const noteList = [];
 | 
			
		||||
 | 
			
		||||
        for (const noteId of childNoteIds) {
 | 
			
		||||
            const noteTreeId = getNoteTreeId(parentNoteId, noteId);
 | 
			
		||||
            const noteTree = notesTreeMap[noteTreeId];
 | 
			
		||||
 | 
			
		||||
            const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.noteId];
 | 
			
		||||
 | 
			
		||||
            const node = {
 | 
			
		||||
                noteId: noteTree.noteId,
 | 
			
		||||
                parentNoteId: noteTree.parentNoteId,
 | 
			
		||||
                noteTreeId: noteTree.noteTreeId,
 | 
			
		||||
                isProtected: noteTree.isProtected,
 | 
			
		||||
                prefix: noteTree.prefix,
 | 
			
		||||
                title: escapeHtml(title),
 | 
			
		||||
                extraClasses: getExtraClasses(noteTree),
 | 
			
		||||
                refKey: noteTree.noteId,
 | 
			
		||||
                expanded: noteTree.isExpanded
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (parentToChildren[noteId] && parentToChildren[noteId].length > 0) {
 | 
			
		||||
                node.folder = true;
 | 
			
		||||
 | 
			
		||||
                if (node.expanded) {
 | 
			
		||||
                    node.children = prepareNoteTreeInner(noteId);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    node.lazy = true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            noteList.push(node);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return noteList;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function expandToNote(notePath, expandOpts) {
 | 
			
		||||
        assertArguments(notePath);
 | 
			
		||||
 | 
			
		||||
        const runPath = getRunPath(notePath);
 | 
			
		||||
 | 
			
		||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
        let parentNoteId = 'root';
 | 
			
		||||
 | 
			
		||||
        for (const childNoteId of runPath) {
 | 
			
		||||
            const node = getNodesByNoteId(childNoteId).find(node => node.data.parentNoteId === parentNoteId);
 | 
			
		||||
 | 
			
		||||
            if (childNoteId === noteId) {
 | 
			
		||||
                return node;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                await node.setExpanded(true, expandOpts);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            parentNoteId = childNoteId;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function activateNode(notePath) {
 | 
			
		||||
        assertArguments(notePath);
 | 
			
		||||
 | 
			
		||||
        const node = await expandToNote(notePath);
 | 
			
		||||
 | 
			
		||||
        await node.setActive();
 | 
			
		||||
 | 
			
		||||
        clearSelectedNodes();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Accepts notePath and tries to resolve it. Part of the path might not be valid because of note moving (which causes
 | 
			
		||||
     * path change) or other corruption, in that case this will try to get some other valid path to the correct note.
 | 
			
		||||
     */
 | 
			
		||||
    function getRunPath(notePath) {
 | 
			
		||||
        assertArguments(notePath);
 | 
			
		||||
 | 
			
		||||
        const path = notePath.split("/").reverse();
 | 
			
		||||
        path.push('root');
 | 
			
		||||
 | 
			
		||||
        const effectivePath = [];
 | 
			
		||||
        let childNoteId = null;
 | 
			
		||||
        let i = 0;
 | 
			
		||||
 | 
			
		||||
        while (true) {
 | 
			
		||||
            if (i >= path.length) {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const parentNoteId = path[i++];
 | 
			
		||||
 | 
			
		||||
            if (childNoteId !== null) {
 | 
			
		||||
                const parents = childToParents[childNoteId];
 | 
			
		||||
 | 
			
		||||
                if (!parents) {
 | 
			
		||||
                    messaging.logError("No parents found for " + childNoteId);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!parents.includes(parentNoteId)) {
 | 
			
		||||
                    console.log(now(), "Did not find parent " + parentNoteId + " for child " + childNoteId);
 | 
			
		||||
 | 
			
		||||
                    if (parents.length > 0) {
 | 
			
		||||
                        console.log(now(), "Available parents:", parents);
 | 
			
		||||
 | 
			
		||||
                        const someNotePath = getSomeNotePath(parents[0]);
 | 
			
		||||
 | 
			
		||||
                        if (someNotePath) { // in case it's root the path may be empty
 | 
			
		||||
                            const pathToRoot = someNotePath.split("/").reverse();
 | 
			
		||||
 | 
			
		||||
                            for (const noteId of pathToRoot) {
 | 
			
		||||
                                effectivePath.push(noteId);
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                    else {
 | 
			
		||||
                        messaging.logError("No parents, can't activate node.");
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (parentNoteId === 'root') {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                effectivePath.push(parentNoteId);
 | 
			
		||||
                childNoteId = parentNoteId;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return effectivePath.reverse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function showParentList(noteId, node) {
 | 
			
		||||
        assertArguments(noteId, node);
 | 
			
		||||
 | 
			
		||||
        const parents = childToParents[noteId];
 | 
			
		||||
 | 
			
		||||
        if (!parents) {
 | 
			
		||||
            throwError("Can't find parents for noteId=" + noteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (parents.length <= 1) {
 | 
			
		||||
            $parentList.hide();
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $parentList.show();
 | 
			
		||||
            $parentListList.empty();
 | 
			
		||||
 | 
			
		||||
            for (const parentNoteId of parents) {
 | 
			
		||||
                const parentNotePath = getSomeNotePath(parentNoteId);
 | 
			
		||||
                // this is to avoid having root notes leading '/'
 | 
			
		||||
                const notePath = parentNotePath ? (parentNotePath + '/' + noteId) : noteId;
 | 
			
		||||
                const title = getNotePathTitle(notePath);
 | 
			
		||||
 | 
			
		||||
                let item;
 | 
			
		||||
 | 
			
		||||
                if (node.getParent().data.noteId === parentNoteId) {
 | 
			
		||||
                    item = $("<span/>").attr("title", "Current note").append(title);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    item = link.createNoteLink(notePath, title);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                $parentListList.append($("<li/>").append(item));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNotePathTitle(notePath) {
 | 
			
		||||
        assertArguments(notePath);
 | 
			
		||||
 | 
			
		||||
        const titlePath = [];
 | 
			
		||||
 | 
			
		||||
        let parentNoteId = 'root';
 | 
			
		||||
 | 
			
		||||
        for (const noteId of notePath.split('/')) {
 | 
			
		||||
            titlePath.push(getNoteTitle(noteId, parentNoteId));
 | 
			
		||||
 | 
			
		||||
            parentNoteId = noteId;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return titlePath.join(' / ');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getSomeNotePath(noteId) {
 | 
			
		||||
        assertArguments(noteId);
 | 
			
		||||
 | 
			
		||||
        const path = [];
 | 
			
		||||
 | 
			
		||||
        let cur = noteId;
 | 
			
		||||
 | 
			
		||||
        while (cur !== 'root') {
 | 
			
		||||
            path.push(cur);
 | 
			
		||||
 | 
			
		||||
            if (!childToParents[cur]) {
 | 
			
		||||
                throwError("Can't find parents for " + cur);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cur = childToParents[cur][0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return path.reverse().join('/');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function setExpandedToServer(noteTreeId, isExpanded) {
 | 
			
		||||
        assertArguments(noteTreeId);
 | 
			
		||||
 | 
			
		||||
        const expandedNum = isExpanded ? 1 : 0;
 | 
			
		||||
 | 
			
		||||
        await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setCurrentNotePathToHash(node) {
 | 
			
		||||
        assertArguments(node);
 | 
			
		||||
 | 
			
		||||
        const currentNotePath = treeUtils.getNotePath(node);
 | 
			
		||||
        const currentNoteTreeId = node.data.noteTreeId;
 | 
			
		||||
 | 
			
		||||
        document.location.hash = currentNotePath;
 | 
			
		||||
 | 
			
		||||
        recentNotes.addRecentNote(currentNoteTreeId, currentNotePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getSelectedNodes(stopOnParents = false) {
 | 
			
		||||
        return getTree().getSelectedNodes(stopOnParents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function clearSelectedNodes() {
 | 
			
		||||
        for (const selectedNode of getSelectedNodes()) {
 | 
			
		||||
            selectedNode.setSelected(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const currentNode = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        if (currentNode) {
 | 
			
		||||
            currentNode.setSelected(true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function initFancyTree(noteTree) {
 | 
			
		||||
        assertArguments(noteTree);
 | 
			
		||||
 | 
			
		||||
        const keybindings = {
 | 
			
		||||
            "del": node => {
 | 
			
		||||
                treeChanges.deleteNodes(getSelectedNodes(true));
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+up": node => {
 | 
			
		||||
                const beforeNode = node.getPrevSibling();
 | 
			
		||||
 | 
			
		||||
                if (beforeNode !== null) {
 | 
			
		||||
                    treeChanges.moveBeforeNode([node], beforeNode);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+down": node => {
 | 
			
		||||
                let afterNode = node.getNextSibling();
 | 
			
		||||
                if (afterNode !== null) {
 | 
			
		||||
                    treeChanges.moveAfterNode([node], afterNode);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+left": node => {
 | 
			
		||||
                treeChanges.moveNodeUpInHierarchy(node);
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+right": node => {
 | 
			
		||||
                let toNode = node.getPrevSibling();
 | 
			
		||||
 | 
			
		||||
                if (toNode !== null) {
 | 
			
		||||
                    treeChanges.moveToNode([node], toNode);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "shift+up": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.UP, true).then(() => {
 | 
			
		||||
                    const currentNode = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
                    if (currentNode.isSelected()) {
 | 
			
		||||
                        node.setSelected(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    currentNode.setSelected(true);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "shift+down": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.DOWN, true).then(() => {
 | 
			
		||||
                    const currentNode = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
                    if (currentNode.isSelected()) {
 | 
			
		||||
                        node.setSelected(false);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    currentNode.setSelected(true);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "f2": node => {
 | 
			
		||||
                editTreePrefix.showDialog(node);
 | 
			
		||||
            },
 | 
			
		||||
            "alt+-": node => {
 | 
			
		||||
                collapseTree(node);
 | 
			
		||||
            },
 | 
			
		||||
            "alt+s": node => {
 | 
			
		||||
                sortAlphabetically(node.data.noteId);
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+a": node => {
 | 
			
		||||
                for (const child of node.getParent().getChildren()) {
 | 
			
		||||
                    child.setSelected(true);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+c": () => {
 | 
			
		||||
                contextMenu.copy(getSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+x": () => {
 | 
			
		||||
                contextMenu.cut(getSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "ctrl+v": node => {
 | 
			
		||||
                contextMenu.pasteInto(node);
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "return": node => {
 | 
			
		||||
                noteEditor.focus();
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "backspace": node => {
 | 
			
		||||
                if (!isTopLevelNode(node)) {
 | 
			
		||||
                    node.getParent().setActive().then(() => clearSelectedNodes());
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin
 | 
			
		||||
            // after opening context menu, standard shortcuts don't work, but they are detected here
 | 
			
		||||
            // so we essentially takeover the standard handling with our implementation.
 | 
			
		||||
            "left": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "right": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "up": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            },
 | 
			
		||||
            "down": node => {
 | 
			
		||||
                node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes());
 | 
			
		||||
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        $tree.fancytree({
 | 
			
		||||
            autoScroll: true,
 | 
			
		||||
            keyboard: false, // we takover keyboard handling in the hotkeys plugin
 | 
			
		||||
            extensions: ["hotkeys", "filter", "dnd", "clones"],
 | 
			
		||||
            source: noteTree,
 | 
			
		||||
            scrollParent: $("#tree"),
 | 
			
		||||
            click: (event, data) => {
 | 
			
		||||
                const targetType = data.targetType;
 | 
			
		||||
                const node = data.node;
 | 
			
		||||
 | 
			
		||||
                if (targetType === 'title' || targetType === 'icon') {
 | 
			
		||||
                    if (!event.ctrlKey) {
 | 
			
		||||
                        node.setActive();
 | 
			
		||||
                        node.setSelected(true);
 | 
			
		||||
 | 
			
		||||
                        clearSelectedNodes();
 | 
			
		||||
                    }
 | 
			
		||||
                    else {
 | 
			
		||||
                        node.setSelected(!node.isSelected());
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            activate: (event, data) => {
 | 
			
		||||
                const node = data.node.data;
 | 
			
		||||
 | 
			
		||||
                setCurrentNotePathToHash(data.node);
 | 
			
		||||
 | 
			
		||||
                noteEditor.switchToNote(node.noteId);
 | 
			
		||||
 | 
			
		||||
                showParentList(node.noteId, data.node);
 | 
			
		||||
            },
 | 
			
		||||
            expand: (event, data) => {
 | 
			
		||||
                setExpandedToServer(data.node.data.noteTreeId, true);
 | 
			
		||||
            },
 | 
			
		||||
            collapse: (event, data) => {
 | 
			
		||||
                setExpandedToServer(data.node.data.noteTreeId, false);
 | 
			
		||||
            },
 | 
			
		||||
            init: (event, data) => {
 | 
			
		||||
                const noteId = treeUtils.getNoteIdFromNotePath(startNotePath);
 | 
			
		||||
 | 
			
		||||
                if (noteIdToTitle[noteId] === undefined) {
 | 
			
		||||
                    // note doesn't exist so don't try to activate it
 | 
			
		||||
                    startNotePath = null;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (startNotePath) {
 | 
			
		||||
                    activateNode(startNotePath);
 | 
			
		||||
 | 
			
		||||
                    // looks like this this doesn't work when triggered immediatelly after activating node
 | 
			
		||||
                    // so waiting a second helps
 | 
			
		||||
                    setTimeout(scrollToCurrentNote, 1000);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            hotkeys: {
 | 
			
		||||
                keydown: keybindings
 | 
			
		||||
            },
 | 
			
		||||
            filter: {
 | 
			
		||||
                autoApply: true,   // Re-apply last filter if lazy data is loaded
 | 
			
		||||
                autoExpand: true, // Expand all branches that contain matches while filtered
 | 
			
		||||
                counter: false,     // Show a badge with number of matching child nodes near parent icons
 | 
			
		||||
                fuzzy: false,      // Match single characters in order, e.g. 'fb' will match 'FooBar'
 | 
			
		||||
                hideExpandedCounter: true,  // Hide counter badge if parent is expanded
 | 
			
		||||
                hideExpanders: false,       // Hide expanders if all child nodes are hidden by filter
 | 
			
		||||
                highlight: true,   // Highlight matches by wrapping inside <mark> tags
 | 
			
		||||
                leavesOnly: false, // Match end nodes only
 | 
			
		||||
                nodata: true,      // Display a 'no data' status node if result is empty
 | 
			
		||||
                mode: "hide"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
 | 
			
		||||
            },
 | 
			
		||||
            dnd: dragAndDropSetup,
 | 
			
		||||
            lazyLoad: function(event, data){
 | 
			
		||||
                const node = data.node.data;
 | 
			
		||||
 | 
			
		||||
                data.result = prepareNoteTreeInner(node.noteId);
 | 
			
		||||
            },
 | 
			
		||||
            clones: {
 | 
			
		||||
                highlightActiveClones: true
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $tree.contextmenu(contextMenu.contextMenuSettings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getTree() {
 | 
			
		||||
        return $tree.fancytree('getTree');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function reload() {
 | 
			
		||||
        const notes = await loadTree();
 | 
			
		||||
 | 
			
		||||
        // this will also reload the note content
 | 
			
		||||
        await getTree().reload(notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getNotePathFromAddress() {
 | 
			
		||||
        return document.location.hash.substr(1); // strip initial #
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function loadTree() {
 | 
			
		||||
        const resp = await server.get('tree');
 | 
			
		||||
        startNotePath = resp.start_note_path;
 | 
			
		||||
 | 
			
		||||
        if (document.location.hash) {
 | 
			
		||||
            startNotePath = getNotePathFromAddress();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        hiddenInAutocomplete = {};
 | 
			
		||||
 | 
			
		||||
        for (const noteId of resp.hiddenInAutocomplete) {
 | 
			
		||||
            hiddenInAutocomplete[noteId] = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return prepareNoteTree(resp.notes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(() => loadTree().then(noteTree => initFancyTree(noteTree)));
 | 
			
		||||
 | 
			
		||||
    function collapseTree(node = null) {
 | 
			
		||||
        if (!node) {
 | 
			
		||||
            node = $tree.fancytree("getRootNode");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        node.setExpanded(false);
 | 
			
		||||
 | 
			
		||||
        node.visit(node => node.setExpanded(false));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'alt+c', () => collapseTree()); // don't use shortened form since collapseTree() accepts argument
 | 
			
		||||
 | 
			
		||||
    function scrollToCurrentNote() {
 | 
			
		||||
        const node = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        if (node) {
 | 
			
		||||
            node.makeVisible({scrollIntoView: true});
 | 
			
		||||
 | 
			
		||||
            node.setFocus();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setNoteTreeBackgroundBasedOnProtectedStatus(noteId) {
 | 
			
		||||
        getNodesByNoteId(noteId).map(node => node.toggleClass("protected", !!node.data.isProtected));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setProtected(noteId, isProtected) {
 | 
			
		||||
        getNodesByNoteId(noteId).map(node => node.data.isProtected = isProtected);
 | 
			
		||||
 | 
			
		||||
        setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getAutocompleteItems(parentNoteId, notePath, titlePath) {
 | 
			
		||||
        if (!parentNoteId) {
 | 
			
		||||
            parentNoteId = 'root';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!parentToChildren[parentNoteId]) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!notePath) {
 | 
			
		||||
            notePath = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!titlePath) {
 | 
			
		||||
            titlePath = '';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const autocompleteItems = [];
 | 
			
		||||
 | 
			
		||||
        for (const childNoteId of parentToChildren[parentNoteId]) {
 | 
			
		||||
            if (hiddenInAutocomplete[childNoteId]) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
 | 
			
		||||
            const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
            autocompleteItems.push({
 | 
			
		||||
                value: childTitlePath + ' (' + childNotePath + ')',
 | 
			
		||||
                label: childTitlePath
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const childItems = getAutocompleteItems(childNoteId, childNotePath, childTitlePath);
 | 
			
		||||
 | 
			
		||||
            for (const childItem of childItems) {
 | 
			
		||||
                autocompleteItems.push(childItem);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return autocompleteItems;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setNoteTitle(noteId, title) {
 | 
			
		||||
        assertArguments(noteId);
 | 
			
		||||
 | 
			
		||||
        noteIdToTitle[noteId] = title;
 | 
			
		||||
 | 
			
		||||
        getNodesByNoteId(noteId).map(clone => treeUtils.setNodeTitleWithPrefix(clone));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function createNewTopLevelNote() {
 | 
			
		||||
        const rootNode = $tree.fancytree("getRootNode");
 | 
			
		||||
 | 
			
		||||
        await createNote(rootNode, "root", "into");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function createNote(node, parentNoteId, target, isProtected) {
 | 
			
		||||
        assertArguments(node, parentNoteId, target);
 | 
			
		||||
 | 
			
		||||
        // if isProtected isn't available (user didn't enter password yet), then note is created as unencrypted
 | 
			
		||||
        // but this is quite weird since user doesn't see WHERE the note is being created so it shouldn't occur often
 | 
			
		||||
        if (!isProtected || !protected_session.isProtectedSessionAvailable()) {
 | 
			
		||||
            isProtected = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const newNoteName = "new note";
 | 
			
		||||
 | 
			
		||||
        const result = await server.post('notes/' + parentNoteId + '/children', {
 | 
			
		||||
            title: newNoteName,
 | 
			
		||||
            target: target,
 | 
			
		||||
            target_noteTreeId: node.data.noteTreeId,
 | 
			
		||||
            isProtected: isProtected
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        setParentChildRelation(result.noteTreeId, parentNoteId, result.noteId);
 | 
			
		||||
 | 
			
		||||
        notesTreeMap[result.noteTreeId] = result;
 | 
			
		||||
 | 
			
		||||
        noteIdToTitle[result.noteId] = newNoteName;
 | 
			
		||||
 | 
			
		||||
        noteEditor.newNoteCreated();
 | 
			
		||||
 | 
			
		||||
        const newNode = {
 | 
			
		||||
            title: newNoteName,
 | 
			
		||||
            noteId: result.noteId,
 | 
			
		||||
            parentNoteId: parentNoteId,
 | 
			
		||||
            refKey: result.noteId,
 | 
			
		||||
            noteTreeId: result.noteTreeId,
 | 
			
		||||
            isProtected: isProtected,
 | 
			
		||||
            extraClasses: getExtraClasses(result.note)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (target === 'after') {
 | 
			
		||||
            await node.appendSibling(newNode).setActive(true);
 | 
			
		||||
        }
 | 
			
		||||
        else if (target === 'into') {
 | 
			
		||||
            if (!node.getChildren() && node.isFolder()) {
 | 
			
		||||
                await node.setExpanded();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                node.addChildren(newNode);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await node.getLastChild().setActive(true);
 | 
			
		||||
 | 
			
		||||
            node.folder = true;
 | 
			
		||||
            node.renderTitle();
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throwError("Unrecognized target: " + target);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        clearSelectedNodes(); // to unmark previously active node
 | 
			
		||||
 | 
			
		||||
        showMessage("Created!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function sortAlphabetically(noteId) {
 | 
			
		||||
        await server.put('notes/' + noteId + '/sort');
 | 
			
		||||
 | 
			
		||||
        await reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function noteExists(noteId) {
 | 
			
		||||
        return !!childToParents[noteId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+o', e => {
 | 
			
		||||
        const node = getCurrentNode();
 | 
			
		||||
        const parentNoteId = node.data.parentNoteId;
 | 
			
		||||
        const isProtected = treeUtils.getParentProtectedStatus(node);
 | 
			
		||||
 | 
			
		||||
        createNote(node, parentNoteId, 'after', isProtected);
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+p', e => {
 | 
			
		||||
        const node = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        createNote(node, node.data.noteId, 'into', node.data.isProtected);
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+del', e => {
 | 
			
		||||
        const node = getCurrentNode();
 | 
			
		||||
 | 
			
		||||
        treeChanges.deleteNodes([node]);
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+.', scrollToCurrentNote);
 | 
			
		||||
 | 
			
		||||
    $(window).bind('hashchange', function() {
 | 
			
		||||
        const notePath = getNotePathFromAddress();
 | 
			
		||||
 | 
			
		||||
        if (getCurrentNotePath() !== notePath) {
 | 
			
		||||
            console.log("Switching to " + notePath + " because of hash change");
 | 
			
		||||
 | 
			
		||||
            activateNode(notePath);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        $(document).bind('keydown', 'alt+left', e => {
 | 
			
		||||
            window.history.back();
 | 
			
		||||
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        $(document).bind('keydown', 'alt+right', e => {
 | 
			
		||||
            window.history.forward();
 | 
			
		||||
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        reload,
 | 
			
		||||
        collapseTree,
 | 
			
		||||
        scrollToCurrentNote,
 | 
			
		||||
        setNoteTreeBackgroundBasedOnProtectedStatus,
 | 
			
		||||
        setProtected,
 | 
			
		||||
        getCurrentNode,
 | 
			
		||||
        expandToNote,
 | 
			
		||||
        activateNode,
 | 
			
		||||
        getCurrentNotePath,
 | 
			
		||||
        getNoteTitle,
 | 
			
		||||
        setCurrentNotePathToHash,
 | 
			
		||||
        getAutocompleteItems,
 | 
			
		||||
        setNoteTitle,
 | 
			
		||||
        createNewTopLevelNote,
 | 
			
		||||
        createNote,
 | 
			
		||||
        setPrefix,
 | 
			
		||||
        getNotePathTitle,
 | 
			
		||||
        removeParentChildRelation,
 | 
			
		||||
        setParentChildRelation,
 | 
			
		||||
        getSelectedNodes,
 | 
			
		||||
        sortAlphabetically,
 | 
			
		||||
        noteExists
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,141 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const noteType = (function() {
 | 
			
		||||
    const $executeScriptButton = $("#execute-script-button");
 | 
			
		||||
    const noteTypeModel = new NoteTypeModel();
 | 
			
		||||
 | 
			
		||||
    function NoteTypeModel() {
 | 
			
		||||
        const self = this;
 | 
			
		||||
 | 
			
		||||
        this.type = ko.observable('text');
 | 
			
		||||
        this.mime = ko.observable('');
 | 
			
		||||
 | 
			
		||||
        this.codeMimeTypes = ko.observableArray([
 | 
			
		||||
            { mime: 'text/x-csrc', title: 'C' },
 | 
			
		||||
            { mime: 'text/x-c++src', title: 'C++' },
 | 
			
		||||
            { mime: 'text/x-csharp', title: 'C#' },
 | 
			
		||||
            { mime: 'text/x-clojure', title: 'Clojure' },
 | 
			
		||||
            { mime: 'text/css', title: 'CSS' },
 | 
			
		||||
            { mime: 'text/x-dockerfile', title: 'Dockerfile' },
 | 
			
		||||
            { mime: 'text/x-erlang', title: 'Erlang' },
 | 
			
		||||
            { mime: 'text/x-feature', title: 'Gherkin' },
 | 
			
		||||
            { mime: 'text/x-go', title: 'Go' },
 | 
			
		||||
            { mime: 'text/x-groovy', title: 'Groovy' },
 | 
			
		||||
            { mime: 'text/x-haskell', title: 'Haskell' },
 | 
			
		||||
            { mime: 'text/html', title: 'HTML' },
 | 
			
		||||
            { mime: 'message/http', title: 'HTTP' },
 | 
			
		||||
            { mime: 'text/x-java', title: 'Java' },
 | 
			
		||||
            { mime: 'application/javascript', title: 'JavaScript' },
 | 
			
		||||
            { mime: 'application/json', title: 'JSON' },
 | 
			
		||||
            { mime: 'text/x-kotlin', title: 'Kotlin' },
 | 
			
		||||
            { mime: 'text/x-lua', title: 'Lua' },
 | 
			
		||||
            { mime: 'text/x-markdown', title: 'Markdown' },
 | 
			
		||||
            { mime: 'text/x-objectivec', title: 'Objective C' },
 | 
			
		||||
            { mime: 'text/x-pascal', title: 'Pascal' },
 | 
			
		||||
            { mime: 'text/x-perl', title: 'Perl' },
 | 
			
		||||
            { mime: 'text/x-php', title: 'PHP' },
 | 
			
		||||
            { mime: 'text/x-python', title: 'Python' },
 | 
			
		||||
            { mime: 'text/x-ruby', title: 'Ruby' },
 | 
			
		||||
            { mime: 'text/x-rustsrc', title: 'Rust' },
 | 
			
		||||
            { mime: 'text/x-scala', title: 'Scala' },
 | 
			
		||||
            { mime: 'text/x-sh', title: 'Shell' },
 | 
			
		||||
            { mime: 'text/x-sql', title: 'SQL' },
 | 
			
		||||
            { mime: 'text/x-swift', title: 'Swift' },
 | 
			
		||||
            { mime: 'text/xml', title: 'XML' },
 | 
			
		||||
            { mime: 'text/x-yaml', title: 'YAML' }
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        this.typeString = function() {
 | 
			
		||||
            const type = self.type();
 | 
			
		||||
            const mime = self.mime();
 | 
			
		||||
 | 
			
		||||
            if (type === 'text') {
 | 
			
		||||
                return 'Text';
 | 
			
		||||
            }
 | 
			
		||||
            else if (type === 'code') {
 | 
			
		||||
                if (!mime) {
 | 
			
		||||
                    return 'Code';
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    const found = self.codeMimeTypes().find(x => x.mime === mime);
 | 
			
		||||
 | 
			
		||||
                    return found ? found.title : mime;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            else if (type === 'render') {
 | 
			
		||||
                return 'Render HTML note';
 | 
			
		||||
            }
 | 
			
		||||
            else if (type === 'file') {
 | 
			
		||||
                return 'Attachment';
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                throwError('Unrecognized type: ' + type);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.isDisabled = function() {
 | 
			
		||||
            return self.type() === "file";
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        async function save() {
 | 
			
		||||
            const note = noteEditor.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
            await server.put('notes/' + note.detail.noteId
 | 
			
		||||
                + '/type/' + encodeURIComponent(self.type())
 | 
			
		||||
                + '/mime/' + encodeURIComponent(self.mime()));
 | 
			
		||||
 | 
			
		||||
            await noteEditor.reload();
 | 
			
		||||
 | 
			
		||||
            // for the note icon to be updated in the tree
 | 
			
		||||
            await noteTree.reload();
 | 
			
		||||
 | 
			
		||||
            self.updateExecuteScriptButtonVisibility();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.selectText = function() {
 | 
			
		||||
            self.type('text');
 | 
			
		||||
            self.mime('');
 | 
			
		||||
 | 
			
		||||
            save();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.selectRender = function() {
 | 
			
		||||
            self.type('render');
 | 
			
		||||
            self.mime('');
 | 
			
		||||
 | 
			
		||||
            save();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.selectCode = function() {
 | 
			
		||||
            self.type('code');
 | 
			
		||||
            self.mime('');
 | 
			
		||||
 | 
			
		||||
            save();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.selectCodeMime = function(el) {
 | 
			
		||||
            self.type('code');
 | 
			
		||||
            self.mime(el.mime);
 | 
			
		||||
 | 
			
		||||
            save();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.updateExecuteScriptButtonVisibility = function() {
 | 
			
		||||
            $executeScriptButton.toggle(self.mime() === 'application/javascript');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        getNoteType: () => noteTypeModel.type(),
 | 
			
		||||
        setNoteType: type => noteTypeModel.type(type),
 | 
			
		||||
 | 
			
		||||
        getNoteMime: () => noteTypeModel.mime(),
 | 
			
		||||
        setNoteMime: mime => {
 | 
			
		||||
            noteTypeModel.mime(mime);
 | 
			
		||||
 | 
			
		||||
            noteTypeModel.updateExecuteScriptButtonVisibility();
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,182 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const protected_session = (function() {
 | 
			
		||||
    const $dialog = $("#protected-session-password-dialog");
 | 
			
		||||
    const $passwordForm = $("#protected-session-password-form");
 | 
			
		||||
    const $password = $("#protected-session-password");
 | 
			
		||||
    const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
			
		||||
 | 
			
		||||
    let protectedSessionDeferred = null;
 | 
			
		||||
    let lastProtectedSessionOperationDate = null;
 | 
			
		||||
    let protectedSessionTimeout = null;
 | 
			
		||||
    let protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
    $(document).ready(() => {
 | 
			
		||||
        server.get('settings/all').then(settings => protectedSessionTimeout = settings.protected_session_timeout);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function setProtectedSessionTimeout(encSessTimeout) {
 | 
			
		||||
        protectedSessionTimeout = encSessTimeout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function ensureProtectedSession(requireProtectedSession, modal) {
 | 
			
		||||
        const dfd = $.Deferred();
 | 
			
		||||
 | 
			
		||||
        if (requireProtectedSession && !isProtectedSessionAvailable()) {
 | 
			
		||||
            protectedSessionDeferred = dfd;
 | 
			
		||||
 | 
			
		||||
            $noteDetailWrapper.hide();
 | 
			
		||||
 | 
			
		||||
            $dialog.dialog({
 | 
			
		||||
                modal: modal,
 | 
			
		||||
                width: 400,
 | 
			
		||||
                open: () => {
 | 
			
		||||
                    if (!modal) {
 | 
			
		||||
                        // dialog steals focus for itself, which is not what we want for non-modal (viewing)
 | 
			
		||||
                        noteTree.getCurrentNode().setFocus();
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            dfd.resolve();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return dfd.promise();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function setupProtectedSession() {
 | 
			
		||||
        const password = $password.val();
 | 
			
		||||
        $password.val("");
 | 
			
		||||
 | 
			
		||||
        const response = await enterProtectedSession(password);
 | 
			
		||||
 | 
			
		||||
        if (!response.success) {
 | 
			
		||||
            showError("Wrong password.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protectedSessionId = response.protectedSessionId;
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog("close");
 | 
			
		||||
 | 
			
		||||
        noteEditor.reload();
 | 
			
		||||
        noteTree.reload();
 | 
			
		||||
 | 
			
		||||
        if (protectedSessionDeferred !== null) {
 | 
			
		||||
            ensureDialogIsClosed($dialog, $password);
 | 
			
		||||
 | 
			
		||||
            $noteDetailWrapper.show();
 | 
			
		||||
 | 
			
		||||
            protectedSessionDeferred.resolve();
 | 
			
		||||
 | 
			
		||||
            protectedSessionDeferred = null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function ensureDialogIsClosed() {
 | 
			
		||||
        // this may fal if the dialog has not been previously opened
 | 
			
		||||
        try {
 | 
			
		||||
            $dialog.dialog('close');
 | 
			
		||||
        }
 | 
			
		||||
        catch (e) {}
 | 
			
		||||
 | 
			
		||||
        $password.val('');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function enterProtectedSession(password) {
 | 
			
		||||
        return await server.post('login/protected', {
 | 
			
		||||
            password: password
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getProtectedSessionId() {
 | 
			
		||||
        return protectedSessionId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function resetProtectedSession() {
 | 
			
		||||
        protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
        // most secure solution - guarantees nothing remained in memory
 | 
			
		||||
        // since this expires because user doesn't use the app, it shouldn't be disruptive
 | 
			
		||||
        reloadApp();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function isProtectedSessionAvailable() {
 | 
			
		||||
        return protectedSessionId !== null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function protectNoteAndSendToServer() {
 | 
			
		||||
        await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
        const note = noteEditor.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        noteEditor.updateNoteFromInputs(note);
 | 
			
		||||
 | 
			
		||||
        note.detail.isProtected = true;
 | 
			
		||||
 | 
			
		||||
        await noteEditor.saveNoteToServer(note);
 | 
			
		||||
 | 
			
		||||
        noteTree.setProtected(note.detail.noteId, note.detail.isProtected);
 | 
			
		||||
 | 
			
		||||
        noteEditor.setNoteBackgroundIfProtected(note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function unprotectNoteAndSendToServer() {
 | 
			
		||||
        await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
        const note = noteEditor.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        noteEditor.updateNoteFromInputs(note);
 | 
			
		||||
 | 
			
		||||
        note.detail.isProtected = false;
 | 
			
		||||
 | 
			
		||||
        await noteEditor.saveNoteToServer(note);
 | 
			
		||||
 | 
			
		||||
        noteTree.setProtected(note.detail.noteId, note.detail.isProtected);
 | 
			
		||||
 | 
			
		||||
        noteEditor.setNoteBackgroundIfProtected(note);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function touchProtectedSession() {
 | 
			
		||||
        if (isProtectedSessionAvailable()) {
 | 
			
		||||
            lastProtectedSessionOperationDate = new Date();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function protectSubTree(noteId, protect) {
 | 
			
		||||
        await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
        await server.put('notes/' + noteId + "/protect-sub-tree/" + (protect ? 1 : 0));
 | 
			
		||||
 | 
			
		||||
        showMessage("Request to un/protect sub tree has finished successfully");
 | 
			
		||||
 | 
			
		||||
        noteTree.reload();
 | 
			
		||||
        noteEditor.reload();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $passwordForm.submit(() => {
 | 
			
		||||
        setupProtectedSession();
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setInterval(() => {
 | 
			
		||||
        if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
 | 
			
		||||
            resetProtectedSession();
 | 
			
		||||
        }
 | 
			
		||||
    }, 5000);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        setProtectedSessionTimeout,
 | 
			
		||||
        ensureProtectedSession,
 | 
			
		||||
        resetProtectedSession,
 | 
			
		||||
        isProtectedSessionAvailable,
 | 
			
		||||
        protectNoteAndSendToServer,
 | 
			
		||||
        unprotectNoteAndSendToServer,
 | 
			
		||||
        getProtectedSessionId,
 | 
			
		||||
        touchProtectedSession,
 | 
			
		||||
        protectSubTree,
 | 
			
		||||
        ensureDialogIsClosed
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const searchTree = (function() {
 | 
			
		||||
    const $tree = $("#tree");
 | 
			
		||||
    const $searchInput = $("input[name='search-text']");
 | 
			
		||||
    const $resetSearchButton = $("button#reset-search-button");
 | 
			
		||||
    const $searchBox = $("#search-box");
 | 
			
		||||
 | 
			
		||||
    $resetSearchButton.click(resetSearch);
 | 
			
		||||
 | 
			
		||||
    function toggleSearch() {
 | 
			
		||||
        if ($searchBox.is(":hidden")) {
 | 
			
		||||
            $searchBox.show();
 | 
			
		||||
            $searchInput.focus();
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            resetSearch();
 | 
			
		||||
 | 
			
		||||
            $searchBox.hide();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function resetSearch() {
 | 
			
		||||
        $searchInput.val("");
 | 
			
		||||
 | 
			
		||||
        getTree().clearFilter();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function getTree() {
 | 
			
		||||
        return $tree.fancytree('getTree');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $searchInput.keyup(async e => {
 | 
			
		||||
        const searchText = $searchInput.val();
 | 
			
		||||
 | 
			
		||||
        if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
 | 
			
		||||
            $resetSearchButton.click();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (e && e.which === $.ui.keyCode.ENTER) {
 | 
			
		||||
            const noteIds = await server.get('notes?search=' + encodeURIComponent(searchText));
 | 
			
		||||
 | 
			
		||||
            for (const noteId of noteIds) {
 | 
			
		||||
                await noteTree.expandToNote(noteId, {noAnimation: true, noEvents: true});
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Pass a string to perform case insensitive matching
 | 
			
		||||
            getTree().filterBranches(node => noteIds.includes(node.data.noteId));
 | 
			
		||||
        }
 | 
			
		||||
    }).focus();
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+s', e => {
 | 
			
		||||
        toggleSearch();
 | 
			
		||||
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        toggleSearch
 | 
			
		||||
    };
 | 
			
		||||
})();
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
const server = (function() {
 | 
			
		||||
    function getHeaders() {
 | 
			
		||||
        let protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
        try { // this is because protected session might not be declared in some cases - like when it's included in migration page
 | 
			
		||||
            protectedSessionId = protected_session.getProtectedSessionId();
 | 
			
		||||
        }
 | 
			
		||||
        catch(e) {}
 | 
			
		||||
 | 
			
		||||
        // headers need to be lowercase because node.js automatically converts them to lower case
 | 
			
		||||
        // so hypothetical protectedSessionId becomes protectedsessionid on the backend
 | 
			
		||||
        return {
 | 
			
		||||
            protected_session_id: protectedSessionId,
 | 
			
		||||
            source_id: glob.sourceId
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function get(url) {
 | 
			
		||||
        return await call('GET', url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function post(url, data) {
 | 
			
		||||
        return await call('POST', url, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function put(url, data) {
 | 
			
		||||
        return await call('PUT', url, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function remove(url) {
 | 
			
		||||
        return await call('DELETE', url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareParams(params) {
 | 
			
		||||
        return params.map(p => {
 | 
			
		||||
            if (typeof p === "function") {
 | 
			
		||||
                return "!@#Function: " + p.toString();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                return p;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function exec(params, script) {
 | 
			
		||||
        if (typeof script === "function") {
 | 
			
		||||
            script = script.toString();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const ret = await post('script/exec', { script: script, params: prepareParams(params) });
 | 
			
		||||
 | 
			
		||||
        return ret.executionResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let i = 1;
 | 
			
		||||
    const reqResolves = {};
 | 
			
		||||
 | 
			
		||||
    async function call(method, url, data) {
 | 
			
		||||
        if (isElectron()) {
 | 
			
		||||
            const ipc = require('electron').ipcRenderer;
 | 
			
		||||
            const requestId = i++;
 | 
			
		||||
 | 
			
		||||
            return new Promise((resolve, reject) => {
 | 
			
		||||
                reqResolves[requestId] = resolve;
 | 
			
		||||
 | 
			
		||||
                console.log(now(), "Request #" + requestId + " to " + method + " " + url);
 | 
			
		||||
 | 
			
		||||
                ipc.send('server-request', {
 | 
			
		||||
                    requestId: requestId,
 | 
			
		||||
                    headers: getHeaders(),
 | 
			
		||||
                    method: method,
 | 
			
		||||
                    url: "/" + baseApiUrl + url,
 | 
			
		||||
                    data: data
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            return await ajax(url, method, data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        const ipc = require('electron').ipcRenderer;
 | 
			
		||||
 | 
			
		||||
        ipc.on('server-response', (event, arg) => {
 | 
			
		||||
            console.log(now(), "Response #" + arg.requestId + ": " + arg.statusCode);
 | 
			
		||||
 | 
			
		||||
            reqResolves[arg.requestId](arg.body);
 | 
			
		||||
 | 
			
		||||
            delete reqResolves[arg.requestId];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function ajax(url, method, data) {
 | 
			
		||||
        const options = {
 | 
			
		||||
            url: baseApiUrl + url,
 | 
			
		||||
            type: method,
 | 
			
		||||
            headers: getHeaders()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (data) {
 | 
			
		||||
            options.data = JSON.stringify(data);
 | 
			
		||||
            options.contentType = "application/json";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await $.ajax(options).catch(e => {
 | 
			
		||||
            const message = "Error when calling " + method + " " + url + ": " + e.status + " - " + e.statusText;
 | 
			
		||||
            showError(message);
 | 
			
		||||
            throwError(message);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        get,
 | 
			
		||||
        post,
 | 
			
		||||
        put,
 | 
			
		||||
        remove,
 | 
			
		||||
        exec,
 | 
			
		||||
        ajax,
 | 
			
		||||
        // don't remove, used from CKEditor image upload!
 | 
			
		||||
        getHeaders
 | 
			
		||||
    }
 | 
			
		||||
})();
 | 
			
		||||
							
								
								
									
										104
									
								
								src/public/javascripts/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/public/javascripts/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import treeUtils from "./tree_utils.js";
 | 
			
		||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
 | 
			
		||||
async function getAutocompleteItems(parentNoteId, notePath, titlePath) {
 | 
			
		||||
    if (!parentNoteId) {
 | 
			
		||||
        parentNoteId = 'root';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parentNote = await treeCache.getNote(parentNoteId);
 | 
			
		||||
    const childNotes = await parentNote.getChildNotes();
 | 
			
		||||
 | 
			
		||||
    if (!childNotes.length) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!notePath) {
 | 
			
		||||
        notePath = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!titlePath) {
 | 
			
		||||
        titlePath = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const autocompleteItems = [];
 | 
			
		||||
 | 
			
		||||
    for (const childNote of childNotes) {
 | 
			
		||||
        if (childNote.hideInAutocomplete) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const childNotePath = (notePath ? (notePath + '/') : '') + childNote.noteId;
 | 
			
		||||
        const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + await treeUtils.getNoteTitle(childNote.noteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!childNote.isProtected || protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
			
		||||
            autocompleteItems.push({
 | 
			
		||||
                value: childTitlePath + ' (' + childNotePath + ')',
 | 
			
		||||
                label: childTitlePath
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const childItems = await getAutocompleteItems(childNote.noteId, childNotePath, childTitlePath);
 | 
			
		||||
 | 
			
		||||
        for (const childItem of childItems) {
 | 
			
		||||
            autocompleteItems.push(childItem);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (parentNoteId === 'root') {
 | 
			
		||||
        console.log(`Generated ${autocompleteItems.length} autocomplete items`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return autocompleteItems;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
 | 
			
		||||
$.ui.autocomplete.filter = (array, terms) => {
 | 
			
		||||
    if (!terms) {
 | 
			
		||||
        return array;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const startDate = new Date();
 | 
			
		||||
 | 
			
		||||
    const results = [];
 | 
			
		||||
    const tokens = terms.toLowerCase().split(" ");
 | 
			
		||||
 | 
			
		||||
    for (const item of array) {
 | 
			
		||||
        const lcLabel = item.label.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
 | 
			
		||||
        if (!found) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this is not completely correct and might cause minor problems with note with names containing this " / "
 | 
			
		||||
        const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
 | 
			
		||||
 | 
			
		||||
        if (lastSegmentIndex !== -1) {
 | 
			
		||||
            const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
 | 
			
		||||
 | 
			
		||||
            // at least some token needs to be in the last segment (leaf note), otherwise this
 | 
			
		||||
            // particular note is not that interesting (query is satisfied by parent note)
 | 
			
		||||
            const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
 | 
			
		||||
 | 
			
		||||
            if (!foundInLastSegment) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        results.push(item);
 | 
			
		||||
 | 
			
		||||
        if (results.length > 100) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("Search took " + (new Date().getTime() - startDate.getTime()) + "ms");
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getAutocompleteItems
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										92
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/javascripts/services/bootstrap.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
import addLinkDialog from '../dialogs/add_link.js';
 | 
			
		||||
import jumpToNoteDialog from '../dialogs/jump_to_note.js';
 | 
			
		||||
import labelsDialog from '../dialogs/labels.js';
 | 
			
		||||
import noteRevisionsDialog from '../dialogs/note_revisions.js';
 | 
			
		||||
import noteSourceDialog from '../dialogs/note_source.js';
 | 
			
		||||
import recentChangesDialog from '../dialogs/recent_changes.js';
 | 
			
		||||
import recentNotesDialog from '../dialogs/recent_notes.js';
 | 
			
		||||
import optionsDialog from '../dialogs/options.js';
 | 
			
		||||
import sqlConsoleDialog from '../dialogs/sql_console.js';
 | 
			
		||||
 | 
			
		||||
import cloning from './cloning.js';
 | 
			
		||||
import contextMenu from './context_menu.js';
 | 
			
		||||
import dragAndDropSetup from './drag_and_drop.js';
 | 
			
		||||
import exportService from './export.js';
 | 
			
		||||
import link from './link.js';
 | 
			
		||||
import messagingService from './messaging.js';
 | 
			
		||||
import noteDetailService from './note_detail.js';
 | 
			
		||||
import noteType from './note_type.js';
 | 
			
		||||
import protected_session from './protected_session.js';
 | 
			
		||||
import searchTreeService from './search_tree.js';
 | 
			
		||||
import ScriptApi from './script_api.js';
 | 
			
		||||
import ScriptContext from './script_context.js';
 | 
			
		||||
import sync from './sync.js';
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import treeChanges from './branches.js';
 | 
			
		||||
import treeUtils from './tree_utils.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import entrypoints from './entrypoints.js';
 | 
			
		||||
import tooltip from './tooltip.js';
 | 
			
		||||
import bundle from "./bundle.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import libraryLoader from "./library_loader.js";
 | 
			
		||||
 | 
			
		||||
// required for CKEditor image upload plugin
 | 
			
		||||
window.glob.getCurrentNode = treeService.getCurrentNode;
 | 
			
		||||
window.glob.getHeaders = server.getHeaders;
 | 
			
		||||
 | 
			
		||||
// required for ESLint plugin
 | 
			
		||||
window.glob.getCurrentNote = noteDetailService.getCurrentNote;
 | 
			
		||||
window.glob.requireLibrary = libraryLoader.requireLibrary;
 | 
			
		||||
window.glob.ESLINT = libraryLoader.ESLINT;
 | 
			
		||||
 | 
			
		||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
			
		||||
    const string = msg.toLowerCase();
 | 
			
		||||
 | 
			
		||||
    let message = "Uncaught error: ";
 | 
			
		||||
 | 
			
		||||
    if (string.indexOf("script error") > -1){
 | 
			
		||||
        message += 'No details available';
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        message += [
 | 
			
		||||
            'Message: ' + msg,
 | 
			
		||||
            'URL: ' + url,
 | 
			
		||||
            'Line: ' + lineNo,
 | 
			
		||||
            'Column: ' + columnNo,
 | 
			
		||||
            'Error object: ' + JSON.stringify(error)
 | 
			
		||||
        ].join(' - ');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    messagingService.logError(message);
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
$("#logout-button").toggle(!utils.isElectron());
 | 
			
		||||
 | 
			
		||||
if (utils.isElectron()) {
 | 
			
		||||
    require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) {
 | 
			
		||||
        // this might occur when day note had to be created
 | 
			
		||||
        if (!await treeCache.getNote(parentNoteId)) {
 | 
			
		||||
            await treeService.reload();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await treeService.activateNode(parentNoteId);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            const node = treeService.getCurrentNode();
 | 
			
		||||
 | 
			
		||||
            treeService.createNote(node, node.data.noteId, 'into', node.data.isProtected);
 | 
			
		||||
        }, 500);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
treeService.showTree();
 | 
			
		||||
 | 
			
		||||
entrypoints.registerEntrypoints();
 | 
			
		||||
 | 
			
		||||
tooltip.setupTooltip();
 | 
			
		||||
 | 
			
		||||
bundle.executeStartupBundles();
 | 
			
		||||
							
								
								
									
										135
									
								
								src/public/javascripts/services/branches.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/public/javascripts/services/branches.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
 | 
			
		||||
async function moveBeforeNode(nodesToMove, beforeNode) {
 | 
			
		||||
    for (const nodeToMove of nodesToMove) {
 | 
			
		||||
        const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-before/' + beforeNode.data.branchId);
 | 
			
		||||
 | 
			
		||||
        if (!resp.success) {
 | 
			
		||||
            alert(resp.message);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function moveAfterNode(nodesToMove, afterNode) {
 | 
			
		||||
    nodesToMove.reverse(); // need to reverse to keep the note order
 | 
			
		||||
 | 
			
		||||
    for (const nodeToMove of nodesToMove) {
 | 
			
		||||
        const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-after/' + afterNode.data.branchId);
 | 
			
		||||
 | 
			
		||||
        if (!resp.success) {
 | 
			
		||||
            alert(resp.message);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await changeNode(nodeToMove, node => node.moveTo(afterNode, 'after'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function moveToNode(nodesToMove, toNode) {
 | 
			
		||||
    for (const nodeToMove of nodesToMove) {
 | 
			
		||||
        const resp = await server.put('branches/' + nodeToMove.data.branchId + '/move-to/' + toNode.data.noteId);
 | 
			
		||||
 | 
			
		||||
        if (!resp.success) {
 | 
			
		||||
            alert(resp.message);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await changeNode(nodeToMove, async node => {
 | 
			
		||||
            // first expand which will force lazy load and only then move the node
 | 
			
		||||
            // if this is not expanded before moving, then lazy load won't happen because it already contains node
 | 
			
		||||
            // this doesn't work if this isn't a folder yet, that's why we expand second time below
 | 
			
		||||
            await toNode.setExpanded(true);
 | 
			
		||||
 | 
			
		||||
            node.moveTo(toNode);
 | 
			
		||||
 | 
			
		||||
            toNode.folder = true;
 | 
			
		||||
            toNode.renderTitle();
 | 
			
		||||
 | 
			
		||||
            // this expands the note in case it become the folder only after the move
 | 
			
		||||
            await toNode.setExpanded(true);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function deleteNodes(nodes) {
 | 
			
		||||
    if (nodes.length === 0 || !confirm('Are you sure you want to delete select note(s) and all the sub-notes?')) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
        await server.remove('branches/' + node.data.branchId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // following code assumes that nodes contain only top-most selected nodes - getSelectedNodes has been
 | 
			
		||||
    // called with stopOnParent=true
 | 
			
		||||
    let next = nodes[nodes.length - 1].getNextSibling();
 | 
			
		||||
 | 
			
		||||
    if (!next) {
 | 
			
		||||
        next = nodes[0].getPrevSibling();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!next && !utils.isTopLevelNode(nodes[0])) {
 | 
			
		||||
        next = nodes[0].getParent();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (next) {
 | 
			
		||||
        // activate next element after this one is deleted so we don't lose focus
 | 
			
		||||
        next.setActive();
 | 
			
		||||
 | 
			
		||||
        treeService.setCurrentNotePathToHash(next);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Note(s) has been deleted.");
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function moveNodeUpInHierarchy(node) {
 | 
			
		||||
    if (utils.isTopLevelNode(node)) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const resp = await server.put('branches/' + node.data.branchId + '/move-after/' + node.getParent().data.branchId);
 | 
			
		||||
 | 
			
		||||
    if (!resp.success) {
 | 
			
		||||
        alert(resp.message);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!utils.isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
 | 
			
		||||
        node.getParent().folder = false;
 | 
			
		||||
        node.getParent().renderTitle();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await changeNode(node, node => node.moveTo(node.getParent(), 'after'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function changeNode(node, func) {
 | 
			
		||||
    utils.assertArguments(node.data.parentNoteId, node.data.noteId);
 | 
			
		||||
 | 
			
		||||
    const childNoteId = node.data.noteId;
 | 
			
		||||
    const oldParentNoteId = node.data.parentNoteId;
 | 
			
		||||
 | 
			
		||||
    await func(node);
 | 
			
		||||
 | 
			
		||||
    const newParentNoteId = node.data.parentNoteId = utils.isTopLevelNode(node) ? 'root' : node.getParent().data.noteId;
 | 
			
		||||
 | 
			
		||||
    await treeCache.moveNote(childNoteId, oldParentNoteId, newParentNoteId);
 | 
			
		||||
 | 
			
		||||
    treeService.setCurrentNotePathToHash(node);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    moveBeforeNode,
 | 
			
		||||
    moveAfterNode,
 | 
			
		||||
    moveToNode,
 | 
			
		||||
    deleteNodes,
 | 
			
		||||
    moveNodeUpInHierarchy
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								src/public/javascripts/services/bundle.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/public/javascripts/services/bundle.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import ScriptContext from "./script_context.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
async function executeBundle(bundle) {
 | 
			
		||||
    const apiContext = ScriptContext(bundle.note, bundle.allNotes);
 | 
			
		||||
 | 
			
		||||
    return await (function () {
 | 
			
		||||
        return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`);
 | 
			
		||||
    }.call(apiContext));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function executeStartupBundles() {
 | 
			
		||||
    const scriptBundles = await server.get("script/startup");
 | 
			
		||||
 | 
			
		||||
    for (const bundle of scriptBundles) {
 | 
			
		||||
        await executeBundle(bundle);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    executeBundle,
 | 
			
		||||
    executeStartupBundles
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/public/javascripts/services/cloning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/services/cloning.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
 | 
			
		||||
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
 | 
			
		||||
    const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
 | 
			
		||||
        prefix: prefix
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (!resp.success) {
 | 
			
		||||
        alert(resp.message);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// beware that first arg is noteId and second is branchId!
 | 
			
		||||
async function cloneNoteAfter(noteId, afterBranchId) {
 | 
			
		||||
    const resp = await server.put('notes/' + noteId + '/clone-after/' + afterBranchId);
 | 
			
		||||
 | 
			
		||||
    if (!resp.success) {
 | 
			
		||||
        alert(resp.message);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    cloneNoteAfter,
 | 
			
		||||
    cloneNoteTo
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										189
									
								
								src/public/javascripts/services/context_menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/public/javascripts/services/context_menu.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,189 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import cloningService from './cloning.js';
 | 
			
		||||
import exportService from './export.js';
 | 
			
		||||
import messagingService from './messaging.js';
 | 
			
		||||
import protectedSessionService from './protected_session.js';
 | 
			
		||||
import treeChangesService from './branches.js';
 | 
			
		||||
import treeUtils from './tree_utils.js';
 | 
			
		||||
import branchPrefixDialog from '../dialogs/branch_prefix.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import syncService from "./sync.js";
 | 
			
		||||
 | 
			
		||||
const $tree = $("#tree");
 | 
			
		||||
 | 
			
		||||
let clipboardIds = [];
 | 
			
		||||
let clipboardMode = null;
 | 
			
		||||
 | 
			
		||||
async function pasteAfter(node) {
 | 
			
		||||
    if (clipboardMode === 'cut') {
 | 
			
		||||
        const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
 | 
			
		||||
 | 
			
		||||
        await treeChangesService.moveAfterNode(nodes, node);
 | 
			
		||||
 | 
			
		||||
        clipboardIds = [];
 | 
			
		||||
        clipboardMode = null;
 | 
			
		||||
    }
 | 
			
		||||
    else if (clipboardMode === 'copy') {
 | 
			
		||||
        for (const noteId of clipboardIds) {
 | 
			
		||||
            await cloningService.cloneNoteAfter(noteId, node.data.branchId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
 | 
			
		||||
    }
 | 
			
		||||
    else if (clipboardIds.length === 0) {
 | 
			
		||||
        // just do nothing
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        infoService.throwError("Unrecognized clipboard mode=" + clipboardMode);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function pasteInto(node) {
 | 
			
		||||
    if (clipboardMode === 'cut') {
 | 
			
		||||
        const nodes = clipboardIds.map(nodeKey => treeUtils.getNodeByKey(nodeKey));
 | 
			
		||||
 | 
			
		||||
        await treeChangesService.moveToNode(nodes, node);
 | 
			
		||||
 | 
			
		||||
        clipboardIds = [];
 | 
			
		||||
        clipboardMode = null;
 | 
			
		||||
    }
 | 
			
		||||
    else if (clipboardMode === 'copy') {
 | 
			
		||||
        for (const noteId of clipboardIds) {
 | 
			
		||||
            await cloningService.cloneNoteTo(noteId, node.data.noteId);
 | 
			
		||||
        }
 | 
			
		||||
        // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places
 | 
			
		||||
    }
 | 
			
		||||
    else if (clipboardIds.length === 0) {
 | 
			
		||||
        // just do nothing
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        infoService.throwError("Unrecognized clipboard mode=" + mode);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function copy(nodes) {
 | 
			
		||||
    clipboardIds = nodes.map(node => node.data.noteId);
 | 
			
		||||
    clipboardMode = 'copy';
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Note(s) have been copied into clipboard.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function cut(nodes) {
 | 
			
		||||
    clipboardIds = nodes.map(node => node.key);
 | 
			
		||||
    clipboardMode = 'cut';
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Note(s) have been cut into clipboard.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const contextMenuOptions = {
 | 
			
		||||
    delegate: "span.fancytree-title",
 | 
			
		||||
    autoFocus: true,
 | 
			
		||||
    menu: [
 | 
			
		||||
        {title: "Insert note here <kbd>Ctrl+O</kbd>", cmd: "insertNoteHere", uiIcon: "ui-icon-plus"},
 | 
			
		||||
        {title: "Insert child note <kbd>Ctrl+P</kbd>", cmd: "insertChildNote", uiIcon: "ui-icon-plus"},
 | 
			
		||||
        {title: "Delete <kbd>Ctrl+Del</kbd>", cmd: "delete", uiIcon: "ui-icon-trash"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Edit branch prefix <kbd>F2</kbd>", cmd: "editBranchPrefix", uiIcon: "ui-icon-pencil"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Protect branch", cmd: "protectBranch", uiIcon: "ui-icon-locked"},
 | 
			
		||||
        {title: "Unprotect branch", cmd: "unprotectBranch", uiIcon: "ui-icon-unlocked"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Copy / clone <kbd>Ctrl+C</kbd>", cmd: "copy", uiIcon: "ui-icon-copy"},
 | 
			
		||||
        {title: "Cut <kbd>Ctrl+X</kbd>", cmd: "cut", uiIcon: "ui-icon-scissors"},
 | 
			
		||||
        {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
        {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Export branch", cmd: "exportBranch", uiIcon: " ui-icon-arrowthick-1-ne"},
 | 
			
		||||
        {title: "Import into branch", cmd: "importBranch", uiIcon: "ui-icon-arrowthick-1-sw"},
 | 
			
		||||
        {title: "----"},
 | 
			
		||||
        {title: "Collapse branch <kbd>Alt+-</kbd>", cmd: "collapseBranch", uiIcon: "ui-icon-minus"},
 | 
			
		||||
        {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
 | 
			
		||||
        {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
 | 
			
		||||
 | 
			
		||||
    ],
 | 
			
		||||
    beforeOpen: async (event, ui) => {
 | 
			
		||||
        const node = $.ui.fancytree.getNode(ui.target);
 | 
			
		||||
        const branch = await treeCache.getBranch(node.data.branchId);
 | 
			
		||||
        const note = await treeCache.getNote(node.data.noteId);
 | 
			
		||||
        const parentNote = await treeCache.getNote(branch.parentNoteId);
 | 
			
		||||
 | 
			
		||||
        // Modify menu entries depending on node status
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && (!parentNote || parentNote.type !== 'search'));
 | 
			
		||||
        $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "insertNoteHere", !parentNote || parentNote.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
 | 
			
		||||
        $tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
 | 
			
		||||
 | 
			
		||||
        // Activate node on right-click
 | 
			
		||||
        node.setActive();
 | 
			
		||||
        // Disable tree keyboard handling
 | 
			
		||||
        ui.menu.prevKeyboard = node.tree.options.keyboard;
 | 
			
		||||
        node.tree.options.keyboard = false;
 | 
			
		||||
    },
 | 
			
		||||
    close: (event, ui) => {},
 | 
			
		||||
    select: (event, ui) => {
 | 
			
		||||
        const node = $.ui.fancytree.getNode(ui.target);
 | 
			
		||||
 | 
			
		||||
        if (ui.cmd === "insertNoteHere") {
 | 
			
		||||
            const parentNoteId = node.data.parentNoteId;
 | 
			
		||||
            const isProtected = treeUtils.getParentProtectedStatus(node);
 | 
			
		||||
 | 
			
		||||
            treeService.createNote(node, parentNoteId, 'after', isProtected);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "insertChildNote") {
 | 
			
		||||
            treeService.createNote(node, node.data.noteId, 'into');
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "editBranchPrefix") {
 | 
			
		||||
            branchPrefixDialog.showDialog(node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "protectBranch") {
 | 
			
		||||
            protectedSessionService.protectBranch(node.data.noteId, true);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "unprotectBranch") {
 | 
			
		||||
            protectedSessionService.protectBranch(node.data.noteId, false);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "copy") {
 | 
			
		||||
            copy(treeService.getSelectedNodes());
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "cut") {
 | 
			
		||||
            cut(treeService.getSelectedNodes());
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "pasteAfter") {
 | 
			
		||||
            pasteAfter(node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "pasteInto") {
 | 
			
		||||
            pasteInto(node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "delete") {
 | 
			
		||||
            treeChangesService.deleteNodes(treeService.getSelectedNodes(true));
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "exportBranch") {
 | 
			
		||||
            exportService.exportBranch(node.data.noteId);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "importBranch") {
 | 
			
		||||
            exportService.importBranch(node.data.noteId);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "collapseBranch") {
 | 
			
		||||
            treeService.collapseTree(node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "forceNoteSync") {
 | 
			
		||||
            syncService.forceNoteSync(node.data.noteId);
 | 
			
		||||
        }
 | 
			
		||||
        else if (ui.cmd === "sortAlphabetically") {
 | 
			
		||||
            treeService.sortAlphabetically(node.data.noteId);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            messagingService.logError("Unknown command: " + ui.cmd);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    pasteAfter,
 | 
			
		||||
    pasteInto,
 | 
			
		||||
    cut,
 | 
			
		||||
    copy,
 | 
			
		||||
    contextMenuOptions
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import treeChangesService from './branches.js';
 | 
			
		||||
 | 
			
		||||
const dragAndDropSetup = {
 | 
			
		||||
    autoExpandMS: 600,
 | 
			
		||||
@@ -49,19 +50,21 @@ const dragAndDropSetup = {
 | 
			
		||||
        const nodeToMove = data.otherNode;
 | 
			
		||||
        nodeToMove.setSelected(true);
 | 
			
		||||
 | 
			
		||||
        const selectedNodes = noteTree.getSelectedNodes();
 | 
			
		||||
        const selectedNodes = treeService.getSelectedNodes();
 | 
			
		||||
 | 
			
		||||
        if (data.hitMode === "before") {
 | 
			
		||||
            treeChanges.moveBeforeNode(selectedNodes, node);
 | 
			
		||||
            treeChangesService.moveBeforeNode(selectedNodes, node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (data.hitMode === "after") {
 | 
			
		||||
            treeChanges.moveAfterNode(selectedNodes, node);
 | 
			
		||||
            treeChangesService.moveAfterNode(selectedNodes, node);
 | 
			
		||||
        }
 | 
			
		||||
        else if (data.hitMode === "over") {
 | 
			
		||||
            treeChanges.moveToNode(selectedNodes, node);
 | 
			
		||||
            treeChangesService.moveToNode(selectedNodes, node);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throw new Exception("Unknown hitMode=" + data.hitMode);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default dragAndDropSetup;
 | 
			
		||||
							
								
								
									
										133
									
								
								src/public/javascripts/services/entrypoints.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/public/javascripts/services/entrypoints.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,133 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
import treeService from "./tree.js";
 | 
			
		||||
import linkService from "./link.js";
 | 
			
		||||
import fileService from "./file.js";
 | 
			
		||||
import noteRevisionsDialog from "../dialogs/note_revisions.js";
 | 
			
		||||
import optionsDialog from "../dialogs/options.js";
 | 
			
		||||
import addLinkDialog from "../dialogs/add_link.js";
 | 
			
		||||
import recentNotesDialog from "../dialogs/recent_notes.js";
 | 
			
		||||
import jumpToNoteDialog from "../dialogs/jump_to_note.js";
 | 
			
		||||
import noteSourceDialog from "../dialogs/note_source.js";
 | 
			
		||||
import recentChangesDialog from "../dialogs/recent_changes.js";
 | 
			
		||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
 | 
			
		||||
import searchTreeService from "./search_tree.js";
 | 
			
		||||
import labelsDialog from "../dialogs/labels.js";
 | 
			
		||||
 | 
			
		||||
function registerEntrypoints() {
 | 
			
		||||
    // hot keys are active also inside inputs and content editables
 | 
			
		||||
    jQuery.hotkeys.options.filterInputAcceptingElements = false;
 | 
			
		||||
    jQuery.hotkeys.options.filterContentEditable = false;
 | 
			
		||||
    jQuery.hotkeys.options.filterTextInputs = false;
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
 | 
			
		||||
 | 
			
		||||
    $("#show-source-button").click(noteSourceDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+u', noteSourceDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#recent-changes-button").click(recentChangesDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#recent-notes-button").click(recentNotesDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#toggle-search-button").click(searchTreeService.toggleSearch);
 | 
			
		||||
    utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
 | 
			
		||||
 | 
			
		||||
    $(".show-labels-button").click(labelsDialog.showDialog);
 | 
			
		||||
    utils.bindShortcut('alt+l', labelsDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    $("#options-button").click(optionsDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('alt+o', sqlConsoleDialog.showDialog);
 | 
			
		||||
 | 
			
		||||
    if (utils.isElectron()) {
 | 
			
		||||
        utils.bindShortcut('alt+left', window.history.back);
 | 
			
		||||
        utils.bindShortcut('alt+right', window.history.forward);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('alt+m', e => $(".hide-toggle").toggleClass("suppressed"));
 | 
			
		||||
 | 
			
		||||
    // hide (toggle) everything except for the note content for distraction free writing
 | 
			
		||||
    utils.bindShortcut('alt+t', e => {
 | 
			
		||||
        const date = new Date();
 | 
			
		||||
        const dateString = utils.formatDateTime(date);
 | 
			
		||||
 | 
			
		||||
        linkService.addTextToEditor(dateString);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('f5', utils.reloadApp);
 | 
			
		||||
 | 
			
		||||
    utils.bindShortcut('ctrl+r', utils.reloadApp);
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+shift+i', () => {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            require('electron').remote.getCurrentWindow().toggleDevTools();
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+f', () => {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            const searchInPage = require('electron-in-page-search').default;
 | 
			
		||||
            const remote = require('electron').remote;
 | 
			
		||||
 | 
			
		||||
            const inPageSearch = searchInPage(remote.getCurrentWebContents());
 | 
			
		||||
 | 
			
		||||
            inPageSearch.openSearchWindow();
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // FIXME: do we really need these at this point?
 | 
			
		||||
    utils.bindShortcut("ctrl+shift+up", () => {
 | 
			
		||||
        const node = treeService.getCurrentNode();
 | 
			
		||||
        node.navigate($.ui.keyCode.UP, true);
 | 
			
		||||
 | 
			
		||||
        $("#note-detail-text").focus();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // FIXME: do we really need these at this point?
 | 
			
		||||
    utils.bindShortcut("ctrl+shift+down", () => {
 | 
			
		||||
        const node = treeService.getCurrentNode();
 | 
			
		||||
        node.navigate($.ui.keyCode.DOWN, true);
 | 
			
		||||
 | 
			
		||||
        $("#note-detail-text").focus();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+-', () => {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
            if (webFrame.getZoomFactor() > 0.2) {
 | 
			
		||||
                webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $(document).bind('keydown', 'ctrl+=', () => {
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            const webFrame = require('electron').webFrame;
 | 
			
		||||
 | 
			
		||||
            webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    $("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());
 | 
			
		||||
 | 
			
		||||
    $("#upload-file-button").click(fileService.uploadFile);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    registerEntrypoints
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/public/javascripts/services/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/public/javascripts/services/export.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
 | 
			
		||||
function exportBranch(noteId) {
 | 
			
		||||
    const url = utils.getHost() + "/api/notes/" + noteId + "/export?protectedSessionId="
 | 
			
		||||
        + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
 | 
			
		||||
 | 
			
		||||
    utils.download(url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let importNoteId;
 | 
			
		||||
 | 
			
		||||
function importBranch(noteId) {
 | 
			
		||||
    importNoteId = noteId;
 | 
			
		||||
 | 
			
		||||
    $("#import-upload").trigger('click');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$("#import-upload").change(async function() {
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('upload', this.files[0]);
 | 
			
		||||
 | 
			
		||||
    await $.ajax({
 | 
			
		||||
        url: baseApiUrl + 'notes/' + importNoteId + '/import',
 | 
			
		||||
        headers: server.getHeaders(),
 | 
			
		||||
        data: formData,
 | 
			
		||||
        type: 'POST',
 | 
			
		||||
        contentType: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
        processData: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    exportBranch,
 | 
			
		||||
    importBranch
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										29
									
								
								src/public/javascripts/services/file.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/public/javascripts/services/file.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
import treeService from "./tree.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
function uploadFile() {
 | 
			
		||||
    $("#file-upload").trigger('click');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$("#file-upload").change(async function() {
 | 
			
		||||
    const formData = new FormData();
 | 
			
		||||
    formData.append('upload', this.files[0]);
 | 
			
		||||
 | 
			
		||||
    const resp = await $.ajax({
 | 
			
		||||
        url: baseApiUrl + 'notes/' + noteDetailService.getCurrentNoteId() + '/upload',
 | 
			
		||||
        headers: server.getHeaders(),
 | 
			
		||||
        data: formData,
 | 
			
		||||
        type: 'POST',
 | 
			
		||||
        contentType: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
        processData: false, // NEEDED, DON'T OMIT THIS
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await treeService.reload();
 | 
			
		||||
 | 
			
		||||
    await treeService.activateNode(resp.noteId);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    uploadFile
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/public/javascripts/services/info.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/public/javascripts/services/info.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import messagingService from "./messaging.js";
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
 | 
			
		||||
function showMessage(message) {
 | 
			
		||||
    console.log(utils.now(), "message: ", message);
 | 
			
		||||
 | 
			
		||||
    $.notify({
 | 
			
		||||
        // options
 | 
			
		||||
        message: message
 | 
			
		||||
    }, {
 | 
			
		||||
        // options
 | 
			
		||||
        type: 'success',
 | 
			
		||||
        delay: 3000
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showError(message, delay = 10000) {
 | 
			
		||||
    console.log(utils.now(), "error: ", message);
 | 
			
		||||
 | 
			
		||||
    $.notify({
 | 
			
		||||
        // options
 | 
			
		||||
        message: message
 | 
			
		||||
    }, {
 | 
			
		||||
        // options
 | 
			
		||||
        type: 'danger',
 | 
			
		||||
        delay: delay
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function throwError(message) {
 | 
			
		||||
    messagingService.logError(message);
 | 
			
		||||
 | 
			
		||||
    throw new Error(message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    showMessage,
 | 
			
		||||
    showError,
 | 
			
		||||
    throwError
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/public/javascripts/services/library_loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/public/javascripts/services/library_loader.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
 | 
			
		||||
 | 
			
		||||
const CODE_MIRROR = {
 | 
			
		||||
    js: [
 | 
			
		||||
        "libraries/codemirror/codemirror.js",
 | 
			
		||||
        "libraries/codemirror/addon/mode/loadmode.js",
 | 
			
		||||
        "libraries/codemirror/addon/fold/xml-fold.js",
 | 
			
		||||
        "libraries/codemirror/addon/edit/matchbrackets.js",
 | 
			
		||||
        "libraries/codemirror/addon/edit/matchtags.js",
 | 
			
		||||
        "libraries/codemirror/addon/search/match-highlighter.js",
 | 
			
		||||
        "libraries/codemirror/mode/meta.js",
 | 
			
		||||
        "libraries/codemirror/addon/lint/lint.js",
 | 
			
		||||
        "libraries/codemirror/addon/lint/eslint.js"
 | 
			
		||||
    ],
 | 
			
		||||
    css: [
 | 
			
		||||
        "libraries/codemirror/codemirror.css",
 | 
			
		||||
        "libraries/codemirror/addon/lint/lint.css"
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ESLINT = {js: ["libraries/eslint.js"]};
 | 
			
		||||
 | 
			
		||||
async function requireLibrary(library) {
 | 
			
		||||
    if (library.css) {
 | 
			
		||||
        library.css.map(cssUrl => requireCss(cssUrl));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (library.js) {
 | 
			
		||||
        for (const scriptUrl of library.js) {
 | 
			
		||||
            await requireScript(scriptUrl);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// we save the promises in case of the same script being required concurrently multiple times
 | 
			
		||||
const loadedScriptPromises = {};
 | 
			
		||||
 | 
			
		||||
async function requireScript(url) {
 | 
			
		||||
    if (!loadedScriptPromises[url]) {
 | 
			
		||||
        loadedScriptPromises[url] = $.ajax({
 | 
			
		||||
            url: url,
 | 
			
		||||
            dataType: "script",
 | 
			
		||||
            cache: true
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await loadedScriptPromises[url];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function requireCss(url) {
 | 
			
		||||
    const css = Array
 | 
			
		||||
        .from(document.querySelectorAll('link'))
 | 
			
		||||
        .map(scr => scr.href);
 | 
			
		||||
 | 
			
		||||
    if (!css.includes(url)) {
 | 
			
		||||
        $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    requireLibrary,
 | 
			
		||||
    CKEDITOR,
 | 
			
		||||
    CODE_MIRROR,
 | 
			
		||||
    ESLINT
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								src/public/javascripts/services/link.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/public/javascripts/services/link.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import noteDetailText from './note_detail_text.js';
 | 
			
		||||
import treeUtils from './tree_utils.js';
 | 
			
		||||
 | 
			
		||||
function getNotePathFromLink(url) {
 | 
			
		||||
    const notePathMatch = /#([A-Za-z0-9/]+)$/.exec(url);
 | 
			
		||||
 | 
			
		||||
    if (notePathMatch === null) {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        return notePathMatch[1];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNodePathFromLabel(label) {
 | 
			
		||||
    const notePathMatch = / \(([A-Za-z0-9/]+)\)/.exec(label);
 | 
			
		||||
 | 
			
		||||
    if (notePathMatch !== null) {
 | 
			
		||||
        return notePathMatch[1];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
    if (!noteTitle) {
 | 
			
		||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
        noteTitle = treeUtils.getNoteTitle(noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const noteLink = $("<a>", {
 | 
			
		||||
        href: 'javascript:',
 | 
			
		||||
        text: noteTitle
 | 
			
		||||
    }).attr('action', 'note')
 | 
			
		||||
        .attr('note-path', notePath);
 | 
			
		||||
 | 
			
		||||
    return noteLink;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function goToLink(e) {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    const $link = $(e.target);
 | 
			
		||||
    let notePath = $link.attr("note-path");
 | 
			
		||||
 | 
			
		||||
    if (!notePath) {
 | 
			
		||||
        const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
 | 
			
		||||
 | 
			
		||||
        if (!address) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (address.startsWith('http')) {
 | 
			
		||||
            window.open(address, '_blank');
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        notePath = getNotePathFromLink(address);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    treeService.activateNode(notePath);
 | 
			
		||||
 | 
			
		||||
    // this is quite ugly hack, but it seems like we can't close the tooltip otherwise
 | 
			
		||||
    $("[role='tooltip']").remove();
 | 
			
		||||
 | 
			
		||||
    if (glob.activeDialog) {
 | 
			
		||||
        try {
 | 
			
		||||
            glob.activeDialog.dialog('close');
 | 
			
		||||
        }
 | 
			
		||||
        catch (e) {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addLinkToEditor(linkTitle, linkHref) {
 | 
			
		||||
    const editor = noteDetailText.getEditor();
 | 
			
		||||
    const doc = editor.document;
 | 
			
		||||
 | 
			
		||||
    doc.enqueueChanges(() => editor.data.insertLink(linkTitle, linkHref), doc.selection);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addTextToEditor(text) {
 | 
			
		||||
    const editor = noteDetailText.getEditor();
 | 
			
		||||
    const doc = editor.document;
 | 
			
		||||
 | 
			
		||||
    doc.enqueueChanges(() => editor.data.insertText(text), doc.selection);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// when click on link popup, in case of internal link, just go the the referenced note instead of default behavior
 | 
			
		||||
// of opening the link in new window/tab
 | 
			
		||||
$(document).on('click', "a[action='note']", goToLink);
 | 
			
		||||
$(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
 | 
			
		||||
$(document).on('dblclick', '#note-detail-text a', goToLink);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getNodePathFromLabel,
 | 
			
		||||
    getNotePathFromLink,
 | 
			
		||||
    createNoteLink,
 | 
			
		||||
    addLinkToEditor,
 | 
			
		||||
    addTextToEditor
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										108
									
								
								src/public/javascripts/services/messaging.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/public/javascripts/services/messaging.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
 | 
			
		||||
const $changesToPushCount = $("#changes-to-push-count");
 | 
			
		||||
 | 
			
		||||
const messageHandlers = [];
 | 
			
		||||
 | 
			
		||||
let ws;
 | 
			
		||||
let lastSyncId;
 | 
			
		||||
let lastPingTs;
 | 
			
		||||
 | 
			
		||||
function logError(message) {
 | 
			
		||||
    console.log(utils.now(), message); // needs to be separate from .trace()
 | 
			
		||||
    console.trace();
 | 
			
		||||
 | 
			
		||||
    if (ws && ws.readyState === 1) {
 | 
			
		||||
        ws.send(JSON.stringify({
 | 
			
		||||
            type: 'log-error',
 | 
			
		||||
            error: message
 | 
			
		||||
        }));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function subscribeToMessages(messageHandler) {
 | 
			
		||||
    messageHandlers.push(messageHandler);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMessage(event) {
 | 
			
		||||
    const message = JSON.parse(event.data);
 | 
			
		||||
 | 
			
		||||
    if (message.type === 'sync') {
 | 
			
		||||
        lastPingTs = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
        if (message.data.length > 0) {
 | 
			
		||||
            console.log(utils.now(), "Sync data: ", message.data);
 | 
			
		||||
 | 
			
		||||
            lastSyncId = message.data[message.data.length - 1].id;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const syncData = message.data.filter(sync => sync.sourceId !== glob.sourceId);
 | 
			
		||||
 | 
			
		||||
        for (const messageHandler of messageHandlers) {
 | 
			
		||||
            messageHandler(syncData);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $changesToPushCount.html(message.changesToPushCount);
 | 
			
		||||
    }
 | 
			
		||||
    else if (message.type === 'sync-hash-check-failed') {
 | 
			
		||||
        infoService.showError("Sync check failed!", 60000);
 | 
			
		||||
    }
 | 
			
		||||
    else if (message.type === 'consistency-checks-failed') {
 | 
			
		||||
        infoService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function connectWebSocket() {
 | 
			
		||||
    const protocol = document.location.protocol === 'https:' ? 'wss' : 'ws';
 | 
			
		||||
 | 
			
		||||
    // use wss for secure messaging
 | 
			
		||||
    const ws = new WebSocket(protocol + "://" + location.host);
 | 
			
		||||
    ws.onopen = event => console.log(utils.now(), "Connected to server with WebSocket");
 | 
			
		||||
    ws.onmessage = handleMessage;
 | 
			
		||||
    ws.onclose = function(){
 | 
			
		||||
        // Try to reconnect in 5 seconds
 | 
			
		||||
        setTimeout(() => connectWebSocket(), 5000);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return ws;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
setTimeout(() => {
 | 
			
		||||
    ws = connectWebSocket();
 | 
			
		||||
 | 
			
		||||
    lastSyncId = glob.maxSyncIdAtLoad;
 | 
			
		||||
    lastPingTs = new Date().getTime();
 | 
			
		||||
    let connectionBrokenNotification = null;
 | 
			
		||||
 | 
			
		||||
    setInterval(async () => {
 | 
			
		||||
        if (new Date().getTime() - lastPingTs > 30000) {
 | 
			
		||||
            if (!connectionBrokenNotification) {
 | 
			
		||||
                connectionBrokenNotification = $.notify({
 | 
			
		||||
                    // options
 | 
			
		||||
                    message: "Lost connection to server"
 | 
			
		||||
                },{
 | 
			
		||||
                    // options
 | 
			
		||||
                    type: 'danger',
 | 
			
		||||
                    delay: 100000000 // keep it until we explicitly close it
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (connectionBrokenNotification) {
 | 
			
		||||
            await connectionBrokenNotification.close();
 | 
			
		||||
            connectionBrokenNotification = null;
 | 
			
		||||
 | 
			
		||||
            infoService.showMessage("Re-connected to server");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ws.send(JSON.stringify({
 | 
			
		||||
            type: 'ping',
 | 
			
		||||
            lastSyncId: lastSyncId
 | 
			
		||||
        }));
 | 
			
		||||
    }, 1000);
 | 
			
		||||
}, 0);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    logError,
 | 
			
		||||
    subscribeToMessages
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										273
									
								
								src/public/javascripts/services/note_detail.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								src/public/javascripts/services/note_detail.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import treeUtils from './tree_utils.js';
 | 
			
		||||
import noteTypeService from './note_type.js';
 | 
			
		||||
import protectedSessionService from './protected_session.js';
 | 
			
		||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import messagingService from "./messaging.js";
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import NoteFull from "../entities/note_full.js";
 | 
			
		||||
import noteDetailCode from './note_detail_code.js';
 | 
			
		||||
import noteDetailText from './note_detail_text.js';
 | 
			
		||||
import noteDetailFile from './note_detail_file.js';
 | 
			
		||||
import noteDetailSearch from './note_detail_search.js';
 | 
			
		||||
import noteDetailRender from './note_detail_render.js';
 | 
			
		||||
 | 
			
		||||
const $noteTitle = $("#note-title");
 | 
			
		||||
 | 
			
		||||
const $noteDetailComponents = $(".note-detail-component");
 | 
			
		||||
 | 
			
		||||
const $protectButton = $("#protect-button");
 | 
			
		||||
const $unprotectButton = $("#unprotect-button");
 | 
			
		||||
const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
			
		||||
const $noteIdDisplay = $("#note-id-display");
 | 
			
		||||
const $labelList = $("#label-list");
 | 
			
		||||
const $labelListInner = $("#label-list-inner");
 | 
			
		||||
const $childrenOverview = $("#children-overview");
 | 
			
		||||
 | 
			
		||||
let currentNote = null;
 | 
			
		||||
 | 
			
		||||
let noteChangeDisabled = false;
 | 
			
		||||
 | 
			
		||||
let isNoteChanged = false;
 | 
			
		||||
 | 
			
		||||
const components = {
 | 
			
		||||
    'code': noteDetailCode,
 | 
			
		||||
    'text': noteDetailText,
 | 
			
		||||
    'file': noteDetailFile,
 | 
			
		||||
    'search': noteDetailSearch,
 | 
			
		||||
    'render': noteDetailRender
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getComponent(type) {
 | 
			
		||||
    if (components[type]) {
 | 
			
		||||
        return components[type];
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        infoService.throwError("Unrecognized type: " + type);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCurrentNote() {
 | 
			
		||||
    return currentNote;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCurrentNoteId() {
 | 
			
		||||
    return currentNote ? currentNote.noteId : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getCurrentNoteType() {
 | 
			
		||||
    const currentNote = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    return currentNote ? currentNote.type : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function noteChanged() {
 | 
			
		||||
    if (noteChangeDisabled) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isNoteChanged = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function reload() {
 | 
			
		||||
    // no saving here
 | 
			
		||||
 | 
			
		||||
    await loadNoteDetail(getCurrentNoteId());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function switchToNote(noteId) {
 | 
			
		||||
    if (getCurrentNoteId() !== noteId) {
 | 
			
		||||
        await saveNoteIfChanged();
 | 
			
		||||
 | 
			
		||||
        await loadNoteDetail(noteId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveNote() {
 | 
			
		||||
    const note = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    note.title = $noteTitle.val();
 | 
			
		||||
    note.content = getComponent(note.type).getContent();
 | 
			
		||||
 | 
			
		||||
    treeService.setNoteTitle(note.noteId, note.title);
 | 
			
		||||
 | 
			
		||||
    await server.put('notes/' + note.noteId, note.dto);
 | 
			
		||||
 | 
			
		||||
    isNoteChanged = false;
 | 
			
		||||
 | 
			
		||||
    if (note.isProtected) {
 | 
			
		||||
        protectedSessionHolder.touchProtectedSession();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Saved!");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function saveNoteIfChanged() {
 | 
			
		||||
    if (!isNoteChanged) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await saveNote();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setNoteBackgroundIfProtected(note) {
 | 
			
		||||
    const isProtected = !!note.isProtected;
 | 
			
		||||
 | 
			
		||||
    $noteDetailWrapper.toggleClass("protected", isProtected);
 | 
			
		||||
    $protectButton.toggle(!isProtected);
 | 
			
		||||
    $unprotectButton.toggle(isProtected);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let isNewNoteCreated = false;
 | 
			
		||||
 | 
			
		||||
function newNoteCreated() {
 | 
			
		||||
    isNewNoteCreated = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function handleProtectedSession() {
 | 
			
		||||
    await protectedSessionService.ensureProtectedSession(currentNote.isProtected, false);
 | 
			
		||||
 | 
			
		||||
    if (currentNote.isProtected) {
 | 
			
		||||
        protectedSessionHolder.touchProtectedSession();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // this might be important if we focused on protected note when not in protected note and we got a dialog
 | 
			
		||||
    // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
 | 
			
		||||
    protectedSessionService.ensureDialogIsClosed();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadNoteDetail(noteId) {
 | 
			
		||||
    currentNote = await loadNote(noteId);
 | 
			
		||||
 | 
			
		||||
    if (isNewNoteCreated) {
 | 
			
		||||
        isNewNoteCreated = false;
 | 
			
		||||
 | 
			
		||||
        $noteTitle.focus().select();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $noteIdDisplay.html(noteId);
 | 
			
		||||
 | 
			
		||||
    await handleProtectedSession();
 | 
			
		||||
 | 
			
		||||
    $noteDetailWrapper.show();
 | 
			
		||||
 | 
			
		||||
    noteChangeDisabled = true;
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        $noteTitle.val(currentNote.title);
 | 
			
		||||
 | 
			
		||||
        noteTypeService.setNoteType(currentNote.type);
 | 
			
		||||
        noteTypeService.setNoteMime(currentNote.mime);
 | 
			
		||||
 | 
			
		||||
        $noteDetailComponents.hide();
 | 
			
		||||
 | 
			
		||||
        await getComponent(currentNote.type).show();
 | 
			
		||||
    }
 | 
			
		||||
    finally {
 | 
			
		||||
        noteChangeDisabled = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setNoteBackgroundIfProtected(currentNote);
 | 
			
		||||
    treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
 | 
			
		||||
 | 
			
		||||
    // after loading new note make sure editor is scrolled to the top
 | 
			
		||||
    $noteDetailWrapper.scrollTop(0);
 | 
			
		||||
 | 
			
		||||
    await loadLabelList();
 | 
			
		||||
 | 
			
		||||
    await showChildrenOverview();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function showChildrenOverview() {
 | 
			
		||||
    const note = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    $childrenOverview.empty();
 | 
			
		||||
 | 
			
		||||
    const notePath = treeService.getCurrentNotePath();
 | 
			
		||||
 | 
			
		||||
    for (const childBranch of await note.getChildBranches()) {
 | 
			
		||||
        const link = $('<a>', {
 | 
			
		||||
            href: 'javascript:',
 | 
			
		||||
            text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId)
 | 
			
		||||
        }).attr('action', 'note').attr('note-path', notePath + '/' + childBranch.noteId);
 | 
			
		||||
 | 
			
		||||
        const childEl = $('<div class="child-overview">').html(link);
 | 
			
		||||
        $childrenOverview.append(childEl);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadLabelList() {
 | 
			
		||||
    const noteId = getCurrentNoteId();
 | 
			
		||||
 | 
			
		||||
    const labels = await server.get('notes/' + noteId + '/labels');
 | 
			
		||||
 | 
			
		||||
    $labelListInner.html('');
 | 
			
		||||
 | 
			
		||||
    if (labels.length > 0) {
 | 
			
		||||
        for (const label of labels) {
 | 
			
		||||
            $labelListInner.append(utils.formatLabel(label) + " ");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $labelList.show();
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        $labelList.hide();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function loadNote(noteId) {
 | 
			
		||||
    const row = await server.get('notes/' + noteId);
 | 
			
		||||
 | 
			
		||||
    return new NoteFull(treeCache, row);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
    const note = getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    getComponent(note.type).focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
messagingService.subscribeToMessages(syncData => {
 | 
			
		||||
    if (syncData.some(sync => sync.entityName === 'notes' && sync.entityId === getCurrentNoteId())) {
 | 
			
		||||
        infoService.showMessage('Reloading note because of background changes');
 | 
			
		||||
 | 
			
		||||
        reload();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
    $noteTitle.on('input', () => {
 | 
			
		||||
        noteChanged();
 | 
			
		||||
 | 
			
		||||
        const title = $noteTitle.val();
 | 
			
		||||
 | 
			
		||||
        treeService.setNoteTitle(getCurrentNoteId(), title);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    noteDetailText.focus();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved
 | 
			
		||||
// this sends the request asynchronously and doesn't wait for result
 | 
			
		||||
$(window).on('beforeunload', () => { saveNoteIfChanged(); }); // don't convert to short form, handler doesn't like returned promise
 | 
			
		||||
 | 
			
		||||
setInterval(saveNoteIfChanged, 5000);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    reload,
 | 
			
		||||
    switchToNote,
 | 
			
		||||
    setNoteBackgroundIfProtected,
 | 
			
		||||
    loadNote,
 | 
			
		||||
    getCurrentNote,
 | 
			
		||||
    getCurrentNoteType,
 | 
			
		||||
    getCurrentNoteId,
 | 
			
		||||
    newNoteCreated,
 | 
			
		||||
    focus,
 | 
			
		||||
    loadLabelList,
 | 
			
		||||
    saveNote,
 | 
			
		||||
    saveNoteIfChanged,
 | 
			
		||||
    noteChanged
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										94
									
								
								src/public/javascripts/services/note_detail_code.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/public/javascripts/services/note_detail_code.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
import libraryLoader from "./library_loader.js";
 | 
			
		||||
import bundleService from "./bundle.js";
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
 | 
			
		||||
let codeEditor = null;
 | 
			
		||||
 | 
			
		||||
const $noteDetailCode = $('#note-detail-code');
 | 
			
		||||
const $executeScriptButton = $("#execute-script-button");
 | 
			
		||||
 | 
			
		||||
async function show() {
 | 
			
		||||
    await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
 | 
			
		||||
 | 
			
		||||
    if (!codeEditor) {
 | 
			
		||||
        CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
 | 
			
		||||
        CodeMirror.keyMap.default["Tab"] = "indentMore";
 | 
			
		||||
 | 
			
		||||
        // these conflict with backward/forward navigation shortcuts
 | 
			
		||||
        delete CodeMirror.keyMap.default["Alt-Left"];
 | 
			
		||||
        delete CodeMirror.keyMap.default["Alt-Right"];
 | 
			
		||||
 | 
			
		||||
        CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
 | 
			
		||||
 | 
			
		||||
        codeEditor = CodeMirror($noteDetailCode[0], {
 | 
			
		||||
            value: "",
 | 
			
		||||
            viewportMargin: Infinity,
 | 
			
		||||
            indentUnit: 4,
 | 
			
		||||
            matchBrackets: true,
 | 
			
		||||
            matchTags: {bothTags: true},
 | 
			
		||||
            highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
 | 
			
		||||
            lint: true,
 | 
			
		||||
            gutters: ["CodeMirror-lint-markers"],
 | 
			
		||||
            lineNumbers: true
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        codeEditor.on('change', noteDetailService.noteChanged);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $noteDetailCode.show();
 | 
			
		||||
 | 
			
		||||
    const currentNote = noteDetailService.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    // this needs to happen after the element is shown, otherwise the editor won't be refreshed
 | 
			
		||||
    codeEditor.setValue(currentNote.content);
 | 
			
		||||
 | 
			
		||||
    const info = CodeMirror.findModeByMIME(currentNote.mime);
 | 
			
		||||
 | 
			
		||||
    if (info) {
 | 
			
		||||
        codeEditor.setOption("mode", info.mime);
 | 
			
		||||
        CodeMirror.autoLoadMode(codeEditor, info.mode);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    codeEditor.refresh();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getContent() {
 | 
			
		||||
    return codeEditor.getValue();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
    codeEditor.focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function executeCurrentNote() {
 | 
			
		||||
    if (noteDetailService.getCurrentNoteType() === 'code') {
 | 
			
		||||
        // make sure note is saved so we load latest changes
 | 
			
		||||
        await noteDetailService.saveNoteIfChanged();
 | 
			
		||||
 | 
			
		||||
        const currentNote = noteDetailService.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        if (currentNote.mime.endsWith("env=frontend")) {
 | 
			
		||||
            const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
            bundleService.executeBundle(bundle);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (currentNote.mime.endsWith("env=backend")) {
 | 
			
		||||
            await server.post('script/run/' + noteDetailService.getCurrentNoteId());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        infoService.showMessage("Note executed");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$(document).bind('keydown', "ctrl+return", executeCurrentNote);
 | 
			
		||||
 | 
			
		||||
$executeScriptButton.click(executeCurrentNote);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    show,
 | 
			
		||||
    getContent,
 | 
			
		||||
    focus
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								src/public/javascripts/services/note_detail_file.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/public/javascripts/services/note_detail_file.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import protectedSessionHolder from "./protected_session_holder.js";
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
 | 
			
		||||
const $noteDetailFile = $('#note-detail-file');
 | 
			
		||||
 | 
			
		||||
const $fileFileName = $("#file-filename");
 | 
			
		||||
const $fileFileType = $("#file-filetype");
 | 
			
		||||
const $fileFileSize = $("#file-filesize");
 | 
			
		||||
const $fileDownload = $("#file-download");
 | 
			
		||||
const $fileOpen = $("#file-open");
 | 
			
		||||
 | 
			
		||||
async function show() {
 | 
			
		||||
    const currentNote = noteDetailService.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
    const labels = await server.get('notes/' + currentNote.noteId + '/labels');
 | 
			
		||||
    const labelMap = utils.toObject(labels, l => [l.name, l.value]);
 | 
			
		||||
 | 
			
		||||
    $noteDetailFile.show();
 | 
			
		||||
 | 
			
		||||
    $fileFileName.text(labelMap.original_file_name);
 | 
			
		||||
    $fileFileSize.text(labelMap.file_size + " bytes");
 | 
			
		||||
    $fileFileType.text(currentNote.mime);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$fileDownload.click(() => utils.download(getFileUrl()));
 | 
			
		||||
 | 
			
		||||
$fileOpen.click(() => {
 | 
			
		||||
    if (utils.isElectron()) {
 | 
			
		||||
        const open = require("open");
 | 
			
		||||
 | 
			
		||||
        open(getFileUrl());
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        window.location.href = getFileUrl();
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function getFileUrl() {
 | 
			
		||||
    // electron needs absolute URL so we extract current host, port, protocol
 | 
			
		||||
    return utils.getHost() + "/api/notes/" + noteDetailService.getCurrentNoteId()
 | 
			
		||||
        + "/download?protectedSessionId=" + encodeURIComponent(protectedSessionHolder.getProtectedSessionId());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    show,
 | 
			
		||||
    getContent: () => null,
 | 
			
		||||
    focus: () => null
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/public/javascripts/services/note_detail_render.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/public/javascripts/services/note_detail_render.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import bundleService from "./bundle.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
 | 
			
		||||
const $noteDetailRender = $('#note-detail-render');
 | 
			
		||||
 | 
			
		||||
async function show() {
 | 
			
		||||
    $noteDetailRender.show();
 | 
			
		||||
 | 
			
		||||
    const bundle = await server.get('script/bundle/' + noteDetailService.getCurrentNoteId());
 | 
			
		||||
 | 
			
		||||
    $noteDetailRender.html(bundle.html);
 | 
			
		||||
 | 
			
		||||
    await bundleService.executeBundle(bundle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    show,
 | 
			
		||||
    getContent: () => null,
 | 
			
		||||
    focus: () => null
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								src/public/javascripts/services/note_detail_search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/services/note_detail_search.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import noteDetailService from "./note_detail.js";
 | 
			
		||||
 | 
			
		||||
const $searchString = $("#search-string");
 | 
			
		||||
const $noteDetailSearch = $('#note-detail-search');
 | 
			
		||||
 | 
			
		||||
function getContent() {
 | 
			
		||||
    JSON.stringify({
 | 
			
		||||
        searchString: $searchString.val()
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function show() {
 | 
			
		||||
    $noteDetailSearch.show();
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        const json = JSON.parse(noteDetailService.getCurrentNote().content);
 | 
			
		||||
 | 
			
		||||
        $searchString.val(json.searchString);
 | 
			
		||||
    }
 | 
			
		||||
    catch (e) {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        $searchString.val('');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $searchString.on('input', noteDetailService.noteChanged);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getContent,
 | 
			
		||||
    show,
 | 
			
		||||
    focus: () => null
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								src/public/javascripts/services/note_detail_text.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/public/javascripts/services/note_detail_text.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
import libraryLoader from "./library_loader.js";
 | 
			
		||||
import noteDetailService from './note_detail.js';
 | 
			
		||||
 | 
			
		||||
const $noteDetailText = $('#note-detail-text');
 | 
			
		||||
 | 
			
		||||
let textEditor = null;
 | 
			
		||||
 | 
			
		||||
async function show() {
 | 
			
		||||
    if (!textEditor) {
 | 
			
		||||
        await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
 | 
			
		||||
 | 
			
		||||
        textEditor = await BalloonEditor.create($noteDetailText[0], {});
 | 
			
		||||
 | 
			
		||||
        textEditor.document.on('change', noteDetailService.noteChanged);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
 | 
			
		||||
    textEditor.setData(noteDetailService.getCurrentNote().content || "<p></p>");
 | 
			
		||||
 | 
			
		||||
    $noteDetailText.show();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getContent() {
 | 
			
		||||
    let content = textEditor.getData();
 | 
			
		||||
 | 
			
		||||
    // if content is only tags/whitespace (typically <p> </p>), then just make it empty
 | 
			
		||||
    // this is important when setting new note to code
 | 
			
		||||
    if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
 | 
			
		||||
        content = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
    $noteDetailText.focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEditor() {
 | 
			
		||||
    return textEditor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    show,
 | 
			
		||||
    getEditor,
 | 
			
		||||
    getContent,
 | 
			
		||||
    focus
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										146
									
								
								src/public/javascripts/services/note_type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/public/javascripts/services/note_type.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,146 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import noteDetailService from './note_detail.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
 | 
			
		||||
const $executeScriptButton = $("#execute-script-button");
 | 
			
		||||
const noteTypeModel = new NoteTypeModel();
 | 
			
		||||
 | 
			
		||||
function NoteTypeModel() {
 | 
			
		||||
    const self = this;
 | 
			
		||||
 | 
			
		||||
    this.type = ko.observable('text');
 | 
			
		||||
    this.mime = ko.observable('');
 | 
			
		||||
 | 
			
		||||
    this.codeMimeTypes = ko.observableArray([
 | 
			
		||||
        { mime: 'text/x-csrc', title: 'C' },
 | 
			
		||||
        { mime: 'text/x-c++src', title: 'C++' },
 | 
			
		||||
        { mime: 'text/x-csharp', title: 'C#' },
 | 
			
		||||
        { mime: 'text/x-clojure', title: 'Clojure' },
 | 
			
		||||
        { mime: 'text/css', title: 'CSS' },
 | 
			
		||||
        { mime: 'text/x-dockerfile', title: 'Dockerfile' },
 | 
			
		||||
        { mime: 'text/x-erlang', title: 'Erlang' },
 | 
			
		||||
        { mime: 'text/x-feature', title: 'Gherkin' },
 | 
			
		||||
        { mime: 'text/x-go', title: 'Go' },
 | 
			
		||||
        { mime: 'text/x-groovy', title: 'Groovy' },
 | 
			
		||||
        { mime: 'text/x-haskell', title: 'Haskell' },
 | 
			
		||||
        { mime: 'text/html', title: 'HTML' },
 | 
			
		||||
        { mime: 'message/http', title: 'HTTP' },
 | 
			
		||||
        { mime: 'text/x-java', title: 'Java' },
 | 
			
		||||
        { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
 | 
			
		||||
        { mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
 | 
			
		||||
        { mime: 'application/json', title: 'JSON' },
 | 
			
		||||
        { mime: 'text/x-kotlin', title: 'Kotlin' },
 | 
			
		||||
        { mime: 'text/x-lua', title: 'Lua' },
 | 
			
		||||
        { mime: 'text/x-markdown', title: 'Markdown' },
 | 
			
		||||
        { mime: 'text/x-objectivec', title: 'Objective C' },
 | 
			
		||||
        { mime: 'text/x-pascal', title: 'Pascal' },
 | 
			
		||||
        { mime: 'text/x-perl', title: 'Perl' },
 | 
			
		||||
        { mime: 'text/x-php', title: 'PHP' },
 | 
			
		||||
        { mime: 'text/x-python', title: 'Python' },
 | 
			
		||||
        { mime: 'text/x-ruby', title: 'Ruby' },
 | 
			
		||||
        { mime: 'text/x-rustsrc', title: 'Rust' },
 | 
			
		||||
        { mime: 'text/x-scala', title: 'Scala' },
 | 
			
		||||
        { mime: 'text/x-sh', title: 'Shell' },
 | 
			
		||||
        { mime: 'text/x-sql', title: 'SQL' },
 | 
			
		||||
        { mime: 'text/x-swift', title: 'Swift' },
 | 
			
		||||
        { mime: 'text/xml', title: 'XML' },
 | 
			
		||||
        { mime: 'text/x-yaml', title: 'YAML' }
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    this.typeString = function() {
 | 
			
		||||
        const type = self.type();
 | 
			
		||||
        const mime = self.mime();
 | 
			
		||||
 | 
			
		||||
        if (type === 'text') {
 | 
			
		||||
            return 'Text';
 | 
			
		||||
        }
 | 
			
		||||
        else if (type === 'code') {
 | 
			
		||||
            if (!mime) {
 | 
			
		||||
                return 'Code';
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                const found = self.codeMimeTypes().find(x => x.mime === mime);
 | 
			
		||||
 | 
			
		||||
                return found ? found.title : mime;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else if (type === 'render') {
 | 
			
		||||
            return 'Render HTML note';
 | 
			
		||||
        }
 | 
			
		||||
        else if (type === 'file') {
 | 
			
		||||
            return 'File';
 | 
			
		||||
        }
 | 
			
		||||
        else if (type === 'search') {
 | 
			
		||||
            // ignore and do nothing, "type" will be hidden since it's not possible to switch to and from search
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            infoService.throwError('Unrecognized type: ' + type);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.isDisabled = function() {
 | 
			
		||||
        return self.type() === "file";
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    async function save() {
 | 
			
		||||
        const note = noteDetailService.getCurrentNote();
 | 
			
		||||
 | 
			
		||||
        await server.put('notes/' + note.noteId
 | 
			
		||||
            + '/type/' + encodeURIComponent(self.type())
 | 
			
		||||
            + '/mime/' + encodeURIComponent(self.mime()));
 | 
			
		||||
 | 
			
		||||
        await noteDetailService.reload();
 | 
			
		||||
 | 
			
		||||
        // for the note icon to be updated in the tree
 | 
			
		||||
        await treeService.reload();
 | 
			
		||||
 | 
			
		||||
        self.updateExecuteScriptButtonVisibility();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.selectText = function() {
 | 
			
		||||
        self.type('text');
 | 
			
		||||
        self.mime('');
 | 
			
		||||
 | 
			
		||||
        save();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.selectRender = function() {
 | 
			
		||||
        self.type('render');
 | 
			
		||||
        self.mime('');
 | 
			
		||||
 | 
			
		||||
        save();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.selectCode = function() {
 | 
			
		||||
        self.type('code');
 | 
			
		||||
        self.mime('');
 | 
			
		||||
 | 
			
		||||
        save();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.selectCodeMime = function(el) {
 | 
			
		||||
        self.type('code');
 | 
			
		||||
        self.mime(el.mime);
 | 
			
		||||
 | 
			
		||||
        save();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    this.updateExecuteScriptButtonVisibility = function() {
 | 
			
		||||
        $executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ko.applyBindings(noteTypeModel, document.getElementById('note-type'));
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getNoteType: () => noteTypeModel.type(),
 | 
			
		||||
    setNoteType: type => noteTypeModel.type(type),
 | 
			
		||||
 | 
			
		||||
    getNoteMime: () => noteTypeModel.mime(),
 | 
			
		||||
    setNoteMime: mime => {
 | 
			
		||||
        noteTypeModel.mime(mime);
 | 
			
		||||
 | 
			
		||||
        noteTypeModel.updateExecuteScriptButtonVisibility();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										142
									
								
								src/public/javascripts/services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/public/javascripts/services/protected_session.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import noteDetailService from './note_detail.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import protectedSessionHolder from './protected_session_holder.js';
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#protected-session-password-dialog");
 | 
			
		||||
const $passwordForm = $("#protected-session-password-form");
 | 
			
		||||
const $password = $("#protected-session-password");
 | 
			
		||||
const $noteDetailWrapper = $("#note-detail-wrapper");
 | 
			
		||||
const $protectButton = $("#protect-button");
 | 
			
		||||
const $unprotectButton = $("#unprotect-button");
 | 
			
		||||
 | 
			
		||||
let protectedSessionDeferred = null;
 | 
			
		||||
 | 
			
		||||
function ensureProtectedSession(requireProtectedSession, modal) {
 | 
			
		||||
    const dfd = $.Deferred();
 | 
			
		||||
 | 
			
		||||
    if (requireProtectedSession && !protectedSessionHolder.isProtectedSessionAvailable()) {
 | 
			
		||||
        protectedSessionDeferred = dfd;
 | 
			
		||||
 | 
			
		||||
        if (treeService.getCurrentNode().data.isProtected) {
 | 
			
		||||
            $noteDetailWrapper.hide();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $dialog.dialog({
 | 
			
		||||
            modal: modal,
 | 
			
		||||
            width: 400,
 | 
			
		||||
            open: () => {
 | 
			
		||||
                if (!modal) {
 | 
			
		||||
                    // dialog steals focus for itself, which is not what we want for non-modal (viewing)
 | 
			
		||||
                    treeService.getCurrentNode().setFocus();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        dfd.resolve();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return dfd.promise();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setupProtectedSession() {
 | 
			
		||||
    const password = $password.val();
 | 
			
		||||
    $password.val("");
 | 
			
		||||
 | 
			
		||||
    const response = await enterProtectedSession(password);
 | 
			
		||||
 | 
			
		||||
    if (!response.success) {
 | 
			
		||||
        infoService.showError("Wrong password.");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protectedSessionHolder.setProtectedSessionId(response.protectedSessionId);
 | 
			
		||||
 | 
			
		||||
    $dialog.dialog("close");
 | 
			
		||||
 | 
			
		||||
    noteDetailService.reload();
 | 
			
		||||
    treeService.reload();
 | 
			
		||||
 | 
			
		||||
    if (protectedSessionDeferred !== null) {
 | 
			
		||||
        ensureDialogIsClosed($dialog, $password);
 | 
			
		||||
 | 
			
		||||
        $noteDetailWrapper.show();
 | 
			
		||||
 | 
			
		||||
        protectedSessionDeferred.resolve();
 | 
			
		||||
 | 
			
		||||
        protectedSessionDeferred = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ensureDialogIsClosed() {
 | 
			
		||||
    // this may fal if the dialog has not been previously opened
 | 
			
		||||
    try {
 | 
			
		||||
        $dialog.dialog('close');
 | 
			
		||||
    }
 | 
			
		||||
    catch (e) {}
 | 
			
		||||
 | 
			
		||||
    $password.val('');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function enterProtectedSession(password) {
 | 
			
		||||
    return await server.post('login/protected', {
 | 
			
		||||
        password: password
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function protectNoteAndSendToServer() {
 | 
			
		||||
    await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
    const note = noteDetailService.getCurrentNote();
 | 
			
		||||
    note.isProtected = true;
 | 
			
		||||
 | 
			
		||||
    await noteDetailService.saveNote(note);
 | 
			
		||||
 | 
			
		||||
    treeService.setProtected(note.noteId, note.isProtected);
 | 
			
		||||
 | 
			
		||||
    noteDetailService.setNoteBackgroundIfProtected(note);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function unprotectNoteAndSendToServer() {
 | 
			
		||||
    await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
    const note = noteDetailService.getCurrentNote();
 | 
			
		||||
    note.isProtected = false;
 | 
			
		||||
 | 
			
		||||
    await noteDetailService.saveNote(note);
 | 
			
		||||
 | 
			
		||||
    treeService.setProtected(note.noteId, note.isProtected);
 | 
			
		||||
 | 
			
		||||
    noteDetailService.setNoteBackgroundIfProtected(note);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function protectBranch(noteId, protect) {
 | 
			
		||||
    await ensureProtectedSession(true, true);
 | 
			
		||||
 | 
			
		||||
    await server.put('notes/' + noteId + "/protect/" + (protect ? 1 : 0));
 | 
			
		||||
 | 
			
		||||
    infoService.showMessage("Request to un/protect sub tree has finished successfully");
 | 
			
		||||
 | 
			
		||||
    treeService.reload();
 | 
			
		||||
    noteDetailService.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$passwordForm.submit(() => {
 | 
			
		||||
    setupProtectedSession();
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
$protectButton.click(protectNoteAndSendToServer);
 | 
			
		||||
$unprotectButton.click(unprotectNoteAndSendToServer);
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    ensureProtectedSession,
 | 
			
		||||
    protectNoteAndSendToServer,
 | 
			
		||||
    unprotectNoteAndSendToServer,
 | 
			
		||||
    protectBranch,
 | 
			
		||||
    ensureDialogIsClosed
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										55
									
								
								src/public/javascripts/services/protected_session_holder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/public/javascripts/services/protected_session_holder.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import utils from "./utils.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
let lastProtectedSessionOperationDate = null;
 | 
			
		||||
let protectedSessionTimeout = null;
 | 
			
		||||
let protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
$(document).ready(() => {
 | 
			
		||||
    server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
setInterval(() => {
 | 
			
		||||
    if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
 | 
			
		||||
        resetProtectedSession();
 | 
			
		||||
    }
 | 
			
		||||
}, 5000);
 | 
			
		||||
 | 
			
		||||
function setProtectedSessionTimeout(encSessTimeout) {
 | 
			
		||||
    protectedSessionTimeout = encSessTimeout;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getProtectedSessionId() {
 | 
			
		||||
    return protectedSessionId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setProtectedSessionId(id) {
 | 
			
		||||
    protectedSessionId = id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resetProtectedSession() {
 | 
			
		||||
    protectedSessionId = null;
 | 
			
		||||
 | 
			
		||||
    // most secure solution - guarantees nothing remained in memory
 | 
			
		||||
    // since this expires because user doesn't use the app, it shouldn't be disruptive
 | 
			
		||||
    utils.reloadApp();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isProtectedSessionAvailable() {
 | 
			
		||||
    return protectedSessionId !== null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function touchProtectedSession() {
 | 
			
		||||
    if (isProtectedSessionAvailable()) {
 | 
			
		||||
        lastProtectedSessionOperationDate = new Date();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    getProtectedSessionId,
 | 
			
		||||
    setProtectedSessionId,
 | 
			
		||||
    resetProtectedSession,
 | 
			
		||||
    isProtectedSessionAvailable,
 | 
			
		||||
    setProtectedSessionTimeout,
 | 
			
		||||
    touchProtectedSession
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										66
									
								
								src/public/javascripts/services/script_api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/public/javascripts/services/script_api.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
import treeService from './tree.js';
 | 
			
		||||
import server from './server.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
import infoService from './info.js';
 | 
			
		||||
 | 
			
		||||
function ScriptApi(startNote, currentNote) {
 | 
			
		||||
    const $pluginButtons = $("#plugin-buttons");
 | 
			
		||||
 | 
			
		||||
    async function activateNote(notePath) {
 | 
			
		||||
        await treeService.activateNode(notePath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function addButtonToToolbar(buttonId, button) {
 | 
			
		||||
        $("#" + buttonId).remove();
 | 
			
		||||
 | 
			
		||||
        button.attr('id', buttonId);
 | 
			
		||||
 | 
			
		||||
        $pluginButtons.append(button);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function prepareParams(params) {
 | 
			
		||||
        if (!params) {
 | 
			
		||||
            return params;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return params.map(p => {
 | 
			
		||||
            if (typeof p === "function") {
 | 
			
		||||
                return "!@#Function: " + p.toString();
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                return p;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function runOnServer(script, params = []) {
 | 
			
		||||
        if (typeof script === "function") {
 | 
			
		||||
            script = script.toString();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const ret = await server.post('script/exec', {
 | 
			
		||||
            script: script,
 | 
			
		||||
            params: prepareParams(params),
 | 
			
		||||
            startNoteId: startNote.noteId,
 | 
			
		||||
            currentNoteId: currentNote.noteId
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return ret.executionResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        startNote: startNote,
 | 
			
		||||
        currentNote: currentNote,
 | 
			
		||||
        addButtonToToolbar,
 | 
			
		||||
        activateNote,
 | 
			
		||||
        getInstanceName: () => window.glob.instanceName,
 | 
			
		||||
        runOnServer,
 | 
			
		||||
        formatDateISO: utils.formatDateISO,
 | 
			
		||||
        parseDate: utils.parseDate,
 | 
			
		||||
        showMessage: infoService.showMessage,
 | 
			
		||||
        showError: infoService.showError,
 | 
			
		||||
        reloadTree: treeService.reload
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScriptApi;
 | 
			
		||||
							
								
								
									
										26
									
								
								src/public/javascripts/services/script_context.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/public/javascripts/services/script_context.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import ScriptApi from './script_api.js';
 | 
			
		||||
import utils from './utils.js';
 | 
			
		||||
 | 
			
		||||
function ScriptContext(startNote, allNotes) {
 | 
			
		||||
    const modules = {};
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        modules: modules,
 | 
			
		||||
        notes: utils.toObject(allNotes, note => [note.noteId, note]),
 | 
			
		||||
        apis: utils.toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
 | 
			
		||||
        require: moduleNoteIds => {
 | 
			
		||||
            return moduleName => {
 | 
			
		||||
                const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId));
 | 
			
		||||
                const note = candidates.find(c => c.title === moduleName);
 | 
			
		||||
 | 
			
		||||
                if (!note) {
 | 
			
		||||
                    throw new Error("Could not find module note " + moduleName);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return modules[note.noteId].exports;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ScriptContext;
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user