Compare commits

...

88 Commits

Author SHA1 Message Date
zadam
f0dfe7d552 release 0.39.4 2020-01-04 22:01:20 +01:00
zadam
3b8b4da149 task context progress fixes 2020-01-04 21:59:28 +01:00
zadam
2150619d62 activateNewNote does not reload whole tree anymore 2020-01-04 21:53:49 +01:00
zadam
acb76e0630 added notification to empty book notes otherwise they look suspiciously empty 2020-01-04 21:24:39 +01:00
zadam
fdb46f9329 fallback image saving without optimization will still compress image 2020-01-04 20:10:30 +01:00
zadam
ca587cccf6 book note type has automatically empty mime type 2020-01-04 19:34:01 +01:00
zadam
571772069a force SQLite to choose particular index for attribute search since it sometimes led to very inefficient query plans 2020-01-04 19:22:16 +01:00
zadam
79e7762c72 indent data notes 2020-01-04 18:44:54 +01:00
zadam
d025cfee1b empty note with just included note should be saved, closes #807 2020-01-04 13:22:07 +01:00
zadam
7793552443 fix display of show sidebar button 2020-01-04 10:05:03 +01:00
zadam
f377a84fa1 hide/show button z-index tweak 2020-01-04 09:21:42 +01:00
zadam
b8f2797abf better behavior of note info widget in tight width 2020-01-04 09:04:08 +01:00
zadam
50431dd55a hide "show/hide sidebar" button in zen mode, fixes #805 2020-01-03 20:12:19 +01:00
zadam
1d3608b7bf fix recent note dialog for deleted notes 2020-01-03 09:04:38 +01:00
zadam
d0c655f66a fix removeAttribute to take into account attribute name, closes #804 2020-01-03 08:55:13 +01:00
zadam
ac75fd2ca3 focus and activate should work together 2020-01-02 19:07:50 +01:00
zadam
3b98428c8c change order of execution to mitigate race conditions 2020-01-02 19:03:54 +01:00
zadam
7d877d0fef release 0.39.3 2020-01-02 10:43:41 +01:00
zadam
cb79f2c7eb updated ckeditor build to support cuttonote 2020-01-02 10:38:29 +01:00
zadam
547a5714ae fancytree 2.34.0 2020-01-01 23:13:49 +01:00
zadam
82420fe5f6 reactivate "cut to note", closes #795 2020-01-01 22:59:51 +01:00
zadam
395913d1bb API docs update 2020-01-01 22:03:27 +01:00
zadam
f3a29b55ba remove @child🧒sorted label from calendar root in demo document since currently @sorted is added automatically in the code, closes #799 2020-01-01 20:58:59 +01:00
zadam
232321f3a4 fix searching multi-valued attributes, closes #800 2020-01-01 20:49:26 +01:00
zadam
51dddb0bbb fix scrolling behavior in firefox 2020-01-01 19:57:57 +01:00
zadam
8b9bf6e46f expand to note to be able to activate note 2020-01-01 19:46:27 +01:00
zadam
631a75deec release 0.39.2-beta 2019-12-30 19:48:54 +01:00
zadam
3f1d0e5872 small template refactoring 2019-12-30 19:46:48 +01:00
zadam
0fe91d0184 include note feature 2019-12-30 19:32:45 +01:00
zadam
2f711a12f8 added "include note" widget to text notes, WIP 2019-12-29 23:46:40 +01:00
zadam
64f32ba38f fix zen mode with new layout, closes #794 2019-12-29 20:48:24 +01:00
zadam
7db4859fb9 Merge remote-tracking branch 'origin/master' 2019-12-29 10:19:11 +01:00
zadam
eee9fcae5c set timeout for the initial sync seed request 2019-12-29 10:19:00 +01:00
zadam
9c4a976342 added some statistic scripts to demo document 2019-12-28 21:52:35 +01:00
zadam
e4a09c6207 fix left pane scrolling, closes #788 2019-12-28 21:37:13 +01:00
zadam
d467db2227 unify API for creating note links 2019-12-28 21:10:02 +01:00
zadam
b8d6ff0542 reset file inputs to allow reuploading the same file again 2019-12-28 19:29:52 +01:00
zadam
a9b8e65c9b force scrolling when width of the content overflows 2019-12-28 19:18:44 +01:00
zadam
bb8b563ece relaunch app after successful sync 2019-12-28 12:55:53 +01:00
zadam
05a8ffb944 small fixes 2019-12-28 10:28:12 +01:00
zadam
2502646a64 release 0.39.1-beta 2019-12-27 21:21:57 +01:00
zadam
3d95d69f80 styling hack for the design to look correct in both FF and chrome 2019-12-27 21:18:14 +01:00
zadam
df751f5d67 fix icon 2019-12-27 21:11:56 +01:00
zadam
4f06b6de78 fix registration of global shortcuts, fixes #786 2019-12-27 20:28:27 +01:00
zadam
d2177cd517 fix detection of desktop build in setup page, closes #787 2019-12-27 20:22:46 +01:00
zadam
0affcf5ad2 move electron-installer-debian to optional dependencies since it can't be installed on windows, #783 2019-12-26 10:02:55 +01:00
zadam
7a416b107b fix electron dep issue in the server version, closes #784 2019-12-25 19:38:28 +01:00
zadam
1ff124dab7 release 0.39.0-beta 2019-12-25 12:18:52 +01:00
zadam
4cb511bad0 fix import with style.css 2019-12-25 12:09:49 +01:00
zadam
73c8d145fa tar export fixes + added code block example to the demo document 2019-12-25 11:56:55 +01:00
zadam
ab79f24729 added content style to tar html export as well 2019-12-25 11:34:45 +01:00
zadam
cec71f65b3 load ckeditor content style for printing to have more similar look to the in-editor, #782 2019-12-25 10:59:45 +01:00
zadam
f75c008154 more sync recovery fixes 2019-12-24 17:49:44 +01:00
zadam
474baa7d95 sync recovery fixes 2019-12-24 16:00:31 +01:00
zadam
a155b6e8d5 create separate window for setup and then main window 2019-12-24 14:42:03 +01:00
zadam
229974e543 added option to enable native title bar (disabled by default) 2019-12-24 12:10:32 +01:00
zadam
6fc19bfb93 Merge branch 'stable' 2019-12-24 11:55:27 +01:00
zadam
ccaa108faa release 0.38.3 2019-12-24 10:51:51 +01:00
zadam
0a72383495 make opening new links from ckeditor more consistent for internal and external links, closes #779 2019-12-24 10:49:16 +01:00
zadam
d389100611 properly cleanup sidebar content after closing tab 2019-12-23 21:46:37 +01:00
zadam
ea7257a5b2 cleaned up experimental attribute pane 2019-12-23 21:13:56 +01:00
zadam
0ebc947fbd visual tweaks 2019-12-23 21:05:47 +01:00
zadam
c89514f9bb saving size and visibility state of the panes 2019-12-23 20:34:29 +01:00
zadam
e0368e395c removed left and right sidebar sizing options 2019-12-23 19:45:59 +01:00
zadam
6986c201dd removed hideTabRowForOneTab option 2019-12-23 19:39:48 +01:00
zadam
bcf163f8a1 css alignment 2019-12-23 17:12:17 +01:00
zadam
15aaead7b9 show/hide switcher for the sidebar 2019-12-23 16:48:34 +01:00
zadam
d29c5c4758 sidebar pulled outside of the tab content and added splitter 2019-12-23 15:50:24 +01:00
zadam
81e2baeee5 blur title buttons after clicking 2019-12-23 13:46:26 +01:00
zadam
4cececafc9 added title bar buttons 2019-12-23 13:34:54 +01:00
zadam
7c8e7a3f4b frameless design with tabs on top, split for left panel 2019-12-23 11:52:45 +01:00
zadam
613d5f93e8 convert css grid design to flex based one 2019-12-23 08:52:57 +01:00
zadam
4f5b23fbf8 tar file export now sets mtime to tar records based on utcDateCreated of a note, closes #487 2019-12-22 10:57:55 +01:00
zadam
a37b9cfc7b steel blue tweaks in demo document 2019-12-21 15:48:36 +01:00
zadam
2bc18bc214 use main border color for tab border 2019-12-21 15:37:51 +01:00
zadam
f31a998c5d path list contains a button to add a new path, closes #611 2019-12-21 13:55:13 +01:00
zadam
5552917533 Merge branch 'stable' 2019-12-21 13:39:12 +01:00
zadam
a9702aa6a2 fix empty checkbox visibility, closes #775 2019-12-21 12:37:24 +01:00
zadam
d1941cc650 Merge branch 'stable' 2019-12-20 21:00:30 +01:00
zadam
f98fa4098f clearer WS error message 2019-12-20 20:17:58 +01:00
zadam
5350496ed4 fix creating note from global ctrl+alt+p shortcut, closes #773 2019-12-20 20:13:21 +01:00
zadam
b62d79044a fix creating note from relation map, closes #771 2019-12-20 19:02:52 +01:00
zadam
0db3722ec2 package updates 2019-12-20 18:04:05 +01:00
zadam
d47403c0e7 implemented sync hash check recovery process 2019-12-18 22:58:30 +01:00
zadam
77311954a1 added sectors for contect check computation 2019-12-18 22:24:13 +01:00
zadam
b7cf4fe96b more general filter parsing 2019-12-18 20:51:48 +01:00
zadam
6d9b702d4c Merge branch 'master' into stable 2019-12-18 20:49:24 +01:00
zadam
882ebdbd8f revert unicode regex since it's still not supported by ff 2019-12-09 23:08:32 +01:00
88 changed files with 2251 additions and 1398 deletions

View File

@@ -2,7 +2,7 @@
<dataSource name="document.db">
<database-model serializer="dbm" dbms="SQLITE" family-id="SQLITE" format-version="4.17">
<root id="1">
<ServerVersion>3.25.1</ServerVersion>
<ServerVersion>3.16.1</ServerVersion>
</root>
<schema id="2" parent="1" name="main">
<Current>1</Current>
@@ -587,7 +587,7 @@ parentNoteId</ColNames>
</column>
<column id="134" parent="16" name="rootpage">
<Position>4</Position>
<DataType>int|0s</DataType>
<DataType>integer|0s</DataType>
</column>
<column id="135" parent="16" name="sql">
<Position>5</Position>

View File

@@ -1,4 +1,4 @@
FROM node:12.13.1-alpine
FROM node:12.14.0-alpine
# Create app directory
WORKDIR /usr/src/app

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=12.13.1
NODE_VERSION=12.14.0
if [ "$1" != "DONTCOPY" ]
then

Binary file not shown.

View File

@@ -553,7 +553,7 @@ class Note extends Entity {
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type &amp;&amp; (value === undefined || value === attribute.value)) {
if (attribute.type === type &amp;&amp; attribute.name === name &amp;&amp; (value === undefined || value === attribute.value)) {
attribute.isDeleted = true;
await attribute.save();

View File

@@ -232,7 +232,7 @@ function BackendScriptApi(currentNote, apiParams) {
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content),
content: JSON.stringify(content, null, '\t'),
type: 'code',
mime: 'application/json'
});

View File

@@ -241,7 +241,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line17">line 17</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line16">line 16</a>
</li></ul></dd>
@@ -357,7 +357,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line13">line 13</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line12">line 12</a>
</li></ul></dd>
@@ -415,7 +415,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line11">line 11</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line10">line 10</a>
</li></ul></dd>
@@ -473,7 +473,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line15">line 15</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line14">line 14</a>
</li></ul></dd>
@@ -549,7 +549,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line21">line 21</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line20">line 20</a>
</li></ul></dd>
@@ -651,7 +651,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line26">line 26</a>
<a href="entities_branch.js.html">entities/branch.js</a>, <a href="entities_branch.js.html#line25">line 25</a>
</li></ul></dd>

View File

@@ -807,7 +807,7 @@
<div class="description">
Activates newly created note. Compared to this.activateNote() also refreshes tree.
Activates newly created note. Compared to this.activateNote() also makes sure that frontend has been fully synced.
</div>
@@ -1366,7 +1366,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line384">line 384</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line390">line 390</a>
</li></ul></dd>
@@ -1546,7 +1546,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line291">line 291</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line297">line 297</a>
</li></ul></dd>
@@ -1679,7 +1679,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line235">line 235</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line241">line 241</a>
</li></ul></dd>
@@ -1785,7 +1785,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line297">line 297</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line303">line 303</a>
</li></ul></dd>
@@ -1891,7 +1891,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line303">line 303</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line309">line 309</a>
</li></ul></dd>
@@ -2050,7 +2050,7 @@
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line350">line 350</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line356">line 356</a>
</li></ul></dd>
@@ -2157,7 +2157,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line228">line 228</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line234">line 234</a>
</li></ul></dd>
@@ -2312,7 +2312,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line359">line 359</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line365">line 365</a>
</li></ul></dd>
@@ -2468,7 +2468,7 @@ if some action needs to happen on only one specific instance.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line200">line 200</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line206">line 206</a>
</li></ul></dd>
@@ -2669,7 +2669,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line212">line 212</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line218">line 218</a>
</li></ul></dd>
@@ -2775,7 +2775,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line341">line 341</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line347">line 347</a>
</li></ul></dd>
@@ -2930,7 +2930,7 @@ otherwise (by e.g. createNoteLink())
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line368">line 368</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line374">line 374</a>
</li></ul></dd>
@@ -3039,7 +3039,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line314">line 314</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line320">line 320</a>
</li></ul></dd>
@@ -3194,7 +3194,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line322">line 322</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line328">line 328</a>
</li></ul></dd>
@@ -3327,7 +3327,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line242">line 242</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line248">line 248</a>
</li></ul></dd>
@@ -3433,7 +3433,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line333">line 333</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line339">line 339</a>
</li></ul></dd>
@@ -3521,7 +3521,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line274">line 274</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line280">line 280</a>
</li></ul></dd>
@@ -3627,7 +3627,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line282">line 282</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line288">line 288</a>
</li></ul></dd>
@@ -3733,7 +3733,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line266">line 266</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line272">line 272</a>
</li></ul></dd>
@@ -3888,7 +3888,7 @@ note.
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line220">line 220</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line226">line 226</a>
</li></ul></dd>
@@ -3924,7 +3924,7 @@ note.
<h4 class="name" id="runOnServer"><span class="type-signature"></span>runOnServer<span class="signature">(script, params)</span><span class="type-signature"> &rarr; {Promise.&lt;*>}</span></h4>
<h4 class="name" id="runOnBackend"><span class="type-signature"></span>runOnBackend<span class="signature">(script, params)</span><span class="type-signature"> &rarr; {Promise.&lt;*>}</span></h4>
@@ -3932,7 +3932,7 @@ note.
<div class="description">
Executes given anonymous function on the server.
Executes given anonymous function on the backend.
Internally this serializes the anonymous function into string and sends it to backend via AJAX.
</div>
@@ -4101,6 +4101,92 @@ Internally this serializes the anonymous function into string and sends it to ba
<h4 class="name" id="runOnServer"><span class="type-signature"></span>runOnServer<span class="signature">()</span><span class="type-signature"></span></h4>
<dl class="details">
<dt class="important tag-deprecated">Deprecated:</dt><dd><ul class="dummy"><li>new name of this API call is runOnBackend so use that</li></ul></dd>
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line166">line 166</a>
</li></ul></dd>
</dl>
@@ -4209,7 +4295,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line188">line 188</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line194">line 194</a>
</li></ul></dd>
@@ -4365,7 +4451,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line170">line 170</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line176">line 176</a>
</li></ul></dd>
@@ -4520,7 +4606,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line377">line 377</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line383">line 383</a>
</li></ul></dd>
@@ -4671,7 +4757,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line328">line 328</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line334">line 334</a>
</li></ul></dd>
@@ -4808,7 +4894,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line258">line 258</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line264">line 264</a>
</li></ul></dd>
@@ -4945,7 +5031,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line250">line 250</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line256">line 256</a>
</li></ul></dd>
@@ -4988,6 +5074,14 @@ Internally this serializes the anonymous function into string and sends it to ba
<div class="description">
Trilium runs in backend and frontend process, when something is changed on the backend from script,
frontend will get asynchronously synchronized.
This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
</div>
@@ -5029,7 +5123,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line389">line 389</a>
<a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line401">line 401</a>
</li></ul></dd>

View File

@@ -34,7 +34,6 @@ class Branch {
this.branchId = row.branchId;
/** @param {string} */
this.noteId = row.noteId;
this.note = null;
/** @param {string} */
this.parentNoteId = row.parentNoteId;
/** @param {int} */

View File

@@ -86,13 +86,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
};
/**
* Activates newly created note. Compared to this.activateNote() also refreshes tree.
* Activates newly created note. Compared to this.activateNote() also makes sure that frontend has been fully synced.
*
* @param {string} notePath (or noteId)
* @return {Promise&lt;void>}
*/
this.activateNewNote = async notePath => {
await treeService.reload();
await ws.waitForMaxKnownSyncId();
await treeService.activateNote(notePath, noteDetailService.focusAndSelectTitle);
};
@@ -153,14 +153,14 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
/**
* Executes given anonymous function on the server.
* Executes given anonymous function on the backend.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @param {string} script - script to be executed on the backend
* @param {Array.&lt;?>} params - list of parameters to the anonymous function to be send to backend
* @return {Promise&lt;*>} return value of the executed function on the backend
*/
this.runOnServer = async (script, params = []) => {
this.runOnBackend = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
@@ -187,6 +187,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
};
/**
* @deprecated new name of this API call is runOnBackend so use that
* @method
*/
this.runOnServer = this.runOnBackend;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "@dateModified =* MONTH AND @log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search
@@ -412,6 +418,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
this.bindGlobalShortcut = utils.bindGlobalShortcut;
/**
* Trilium runs in backend and frontend process, when something is changed on the backend from script,
* frontend will get asynchronously synchronized.
*
* This method returns a promise which resolves once all the backend -> frontend synchronization is finished.
* Typical use case is when new note has been created, we should wait until it is synced into frontend and only then activate it.
*
* @method
*/
this.waitUntilSynced = ws.waitForMaxKnownSyncId;

View File

@@ -1,86 +1,17 @@
'use strict';
const {app, globalShortcut, BrowserWindow} = require('electron');
const path = require('path');
const log = require('./src/services/log');
const {app, globalShortcut} = require('electron');
const sqlInit = require('./src/services/sql_init');
const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const env = require('./src/services/env');
const keyboardActionsService = require('./src/services/keyboard_actions');
const appIconService = require('./src/services/app_icon');
const windowStateKeeper = require('electron-window-state');
const windowService = require('./src/services/window');
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
appIconService.installLocalAppIcon();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array
mainWindow = null;
}
async function createMainWindow() {
await sqlInit.dbConnection;
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
}
const mainWindowState = windowStateKeeper({
// default window width & height so it's usable on 1600 * 900 display (including some extra panels etc.)
defaultWidth: 1200,
defaultHeight: 800
});
const win = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
nodeIntegration: true
},
icon: path.join(__dirname, 'images/app-icons/png/256x256' + (env.isDev() ? '-dev' : '') + '.png')
});
mainWindowState.manage(win);
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port);
win.on('closed', onClosed);
win.webContents.on('new-window', (e, url) => {
if (url !== win.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}
});
// prevent drag & drop to navigate away from trilium
win.webContents.on('will-navigate', (ev, targetUrl) => {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});
return win;
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
@@ -90,50 +21,23 @@ app.on('window-all-closed', () => {
}
});
app.on('activate', () => {
if (!mainWindow) {
mainWindow = createMainWindow();
}
});
async function registerGlobalShortcuts() {
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
await sqlInit.dbConnection;
registerGlobalShortcuts();
// if schema doesn't exist -> setup process
// if schema exists, then we need to wait until the migration process is finished
if (await sqlInit.schemaExists()) {
await sqlInit.dbReady;
await windowService.createMainWindow();
}
else {
await windowService.createSetupWindow();
}
await windowService.registerGlobalShortcuts();
});
app.on('will-quit', () => {

View File

@@ -0,0 +1,310 @@
/*
* !!!!!!! This stylesheet is heavily modified compared to the original for similarity with in-editor look !!!!!!!
* This is used for printing and tar HTML export
*
* CKEditor 5 (v15.0.0) content styles.
* Generated on Wed, 27 Nov 2019 13:26:13 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/content-styles.html
*/
:root {
--ck-highlight-marker-blue: #72cdfd;
--ck-highlight-marker-green: #63f963;
--ck-highlight-marker-pink: #fc7999;
--ck-highlight-marker-yellow: #fdfd77;
--ck-highlight-pen-green: #118800;
--ck-highlight-pen-red: #e91313;
--ck-image-style-spacing: 1.5em;
--ck-todo-list-checkmark-size: 16px;
font-family: Arial, Sans-Serif;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list {
list-style: none;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li {
margin-bottom: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list li .todo-list {
margin-top: 5px;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::before {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label > input[checked]::after {
border-color: hsl(126, 64%, 41%);
}
/* ckeditor5-list/theme/todolist.css */
.todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* ckeditor5-image/theme/image.css */
.image {
display: table;
clear: both;
text-align: center;
margin: 1em auto;
}
/* ckeditor5-image/theme/image.css */
.image > img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 50px;
}
/* ckeditor5-block-quote/theme/blockquote.css */
blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized img {
width: 100%;
}
/* ckeditor5-image/theme/imageresize.css */
.image.image_resized > figcaption {
display: block;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side,
.image-style-align-left,
.image-style-align-center,
.image-style-align-right {
max-width: 50%;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* ckeditor5-image/theme/imagestyle.css */
.image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* ckeditor5-media-embed/theme/mediaembed.css */
.media {
clear: both;
margin: 1em 0;
display: block;
min-width: 15em;
}
/* ckeditor5-table/theme/table.css */
.table {
display: table;
margin: 0;
}
/* ckeditor5-table/theme/table.css */
.table table {
border-collapse: collapse;
border-spacing: 0;
border: 1px double hsl(0, 0%, 70%);
}
/* ckeditor5-table/theme/table.css */
.table table td,
.table table th {
min-width: 2em;
padding: .4em;
border: 1px solid #d9d9d9;
}
/* ckeditor5-table/theme/table.css */
.table table th {
font-weight: bold;
background-color: #f5f5f5;
text-align: left;
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* ckeditor5-highlight/theme/highlight.css */
.marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* ckeditor5-highlight/theme/highlight.css */
.pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: #fff;
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* ckeditor5-basic-styles/theme/code.css */
code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* ckeditor5-image/theme/imagecaption.css */
.image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: hsl(0, 0%, 20%);
background-color: hsl(0, 0%, 97%);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* ckeditor5-horizontal-line/theme/horizontalline.css */
hr {
border-width: 1px 0 0;
border-style: solid;
border-color: hsl(0, 0%, 37%);
margin: 0;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre {
padding: 1em;
color: #353535;
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
}
/* ckeditor5-code-block/theme/codeblock.css */
pre code {
background: unset;
padding: 0;
border-radius: 0;
}
.ck-widget__selection-handle {
display: none;
}
@media print {
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break {
padding: 0;
}
/* ckeditor5-page-break/theme/pagebreak.css */
.page-break::after {
display: none;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,4 @@
/*! jQuery Fancytree Plugin - 2.33.0 - 2019-10-29T08:00:07Z
/*! jQuery Fancytree Plugin - 2.34.0 - 2019-12-26T14:16:19Z
* https://github.com/mar10/fancytree
* Copyright (c) 2019 Martin Wendt; Licensed MIT
*/
@@ -1365,8 +1365,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
/** Core Fancytree module.
@@ -4156,11 +4156,18 @@ var uniqueId = $.fn.extend( {
delete this._tempCache[key];
return null;
},
/* Check if this tree has extension `name` enabled.
*
* @param {string} name name of the required extension
*/
_usesExtension: function(name) {
return $.inArray(name, this.options.extensions) >= 0;
},
/* Check if current extensions dependencies are met and throw an error if not.
*
* This method may be called inside the `treeInit` hook for custom extensions.
*
* @param {string} extension name of the required extension
* @param {string} name name of the required extension
* @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present
* @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter)
* @param {string} [message] optional error message (defaults to a descriptve error message)
@@ -4417,6 +4424,13 @@ var uniqueId = $.fn.extend( {
consoleApply("log", arguments);
}
},
/** Destroy this widget, restore previous markup and cleanup resources.
*
* @since 2.34
*/
destroy: function() {
this.widget.destroy();
},
/** Enable (or disable) the tree control.
*
* @param {boolean} [flag=true] pass false to disable
@@ -4795,6 +4809,20 @@ var uniqueId = $.fn.extend( {
* @returns {boolean}
*/
hasFocus: function() {
// var ae = document.activeElement,
// hasFocus = !!(
// ae && $(ae).closest(".fancytree-container").length
// );
// if (hasFocus !== !!this._hasFocus) {
// this.warn(
// "hasFocus(): fix inconsistent container state, now: " +
// hasFocus
// );
// this._hasFocus = hasFocus;
// this.$container.toggleClass("fancytree-treefocus", hasFocus);
// }
// return hasFocus;
return !!this._hasFocus;
},
/** Write to browser console if debugLevel >= 3 (prepending tree name)
@@ -5120,6 +5148,26 @@ var uniqueId = $.fn.extend( {
setOption: function(optionName, value) {
return this.widget.option(optionName, value);
},
/**
* Call console.time() when in debug mode (verbose >= 4).
*
* @param {string} label
*/
debugTime: function(label) {
if (this.options.debugLevel >= 4) {
window.console.time(this + " - " + label);
}
},
/**
* Call console.timeEnd() when in debug mode (verbose >= 4).
*
* @param {string} label
*/
debugTimeEnd: function(label) {
if (this.options.debugLevel >= 4) {
window.console.timeEnd(this + " - " + label);
}
},
/**
* Return all nodes as nested list of {@link NodeData}.
*
@@ -5185,7 +5233,7 @@ var uniqueId = $.fn.extend( {
* @since 2.28
*/
visitRows: function(fn, opts) {
if (!this.rootNode.children) {
if (!this.rootNode.hasChildren()) {
return false;
}
if (opts && opts.reverse) {
@@ -5314,7 +5362,8 @@ var uniqueId = $.fn.extend( {
/**
* These additional methods of the {@link Fancytree} class are 'hook functions'
* that can be used and overloaded by extensions.
* (See <a href="https://github.com/mar10/fancytree/wiki/TutorialExtensions">writing extensions</a>.)
*
* @see [writing extensions](https://github.com/mar10/fancytree/wiki/TutorialExtensions)
* @mixin Fancytree_Hooks
*/
$.extend(
@@ -6005,6 +6054,7 @@ var uniqueId = $.fn.extend( {
* Call this method to create new nodes, or after the strucuture
* was changed (e.g. after moving this node or adding/removing children)
* nodeRenderTitle() and nodeRenderStatus() are implied.
*
* ```html
* <li id='KEY' ftnode=NODE>
* <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'>
@@ -7493,14 +7543,28 @@ var uniqueId = $.fn.extend( {
*/
/**
* The plugin (derrived from <a href=" http://api.jqueryui.com/jQuery.widget/">jQuery.Widget</a>).<br>
* This constructor is not called directly. Use `$(selector).fancytree({})`
* to initialize the plugin instead.<br>
* <pre class="sh_javascript sunlight-highlight-javascript">// Access widget methods and members:
* var tree = $("#tree").fancytree("getTree");
* var node = $("#tree").fancytree("getActiveNode", "1234");
* </pre>
* The plugin (derrived from [jQuery.Widget](http://api.jqueryui.com/jQuery.widget/)).
*
* **Note:**
* These methods implement the standard jQuery UI widget API.
* It is recommended to use methods of the {Fancytree} instance instead
*
* @example
* // DEPRECATED: Access jQuery UI widget methods and members:
* var tree = $("#tree").fancytree("getTree", "#myTree");
* var node = $.ui.fancytree.getTree("#tree").getActiveNode();
*
* // RECOMMENDED: Use the Fancytree object API
* var tree = $.ui.fancytree.getTree("#myTree");
* var node = tree.getActiveNode();
*
* // or you may already have stored the tree instance upon creation:
* import {createTree, version} from 'jquery.fancytree'
* const tree = createTree('#tree', { ... });
* var node = tree.getActiveNode();
*
* @see {Fancytree_Static#getTree}
* @deprecated Use methods of the {Fancytree} instance instead
* @mixin Fancytree_Widget
*/
@@ -7584,6 +7648,17 @@ var uniqueId = $.fn.extend( {
lazyLoad: null,
postProcess: null,
},
_deprecationWarning: function(name) {
var tree = this.tree;
if (tree && tree.options.debugLevel >= 3) {
tree.warn(
"$().fancytree('" +
name +
"') is deprecated (see https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Widget.html"
);
}
},
/* Set up the widget, Called on first $().fancytree() */
_create: function() {
this.tree = new Fancytree(this);
@@ -7701,11 +7776,11 @@ var uniqueId = $.fn.extend( {
},
/** Use the destroy method to clean up any modifications your widget has made to the DOM */
destroy: function() {
_destroy: function() {
this._unbind();
this.tree._callHook("treeDestroy", this.tree);
// In jQuery UI 1.8, you must invoke the destroy method from the base widget
$.Widget.prototype.destroy.call(this);
// $.Widget.prototype.destroy.call(this);
// TODO: delete tree and nodes to make garbage collect easier?
// TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method
},
@@ -7898,27 +7973,35 @@ var uniqueId = $.fn.extend( {
},
/** Return the active node or null.
* @returns {FancytreeNode}
* @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
*/
getActiveNode: function() {
this._deprecationWarning("getActiveNode");
return this.tree.activeNode;
},
/** Return the matching node or null.
* @param {string} key
* @returns {FancytreeNode}
* @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
*/
getNodeByKey: function(key) {
this._deprecationWarning("getNodeByKey");
return this.tree.getNodeByKey(key);
},
/** Return the invisible system root node.
* @returns {FancytreeNode}
* @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
*/
getRootNode: function() {
this._deprecationWarning("getRootNode");
return this.tree.rootNode;
},
/** Return the current tree instance.
* @returns {Fancytree}
* @deprecated Use `$.ui.fancytree.getTree()` instead (<a href="Fancytree_Widget.html">example above</a>).
*/
getTree: function() {
this._deprecationWarning("getTree");
return this.tree;
},
}
@@ -7928,12 +8011,14 @@ var uniqueId = $.fn.extend( {
FT = $.ui.fancytree;
/**
* Static members in the `$.ui.fancytree` namespace.<br>
* <br>
* <pre class="sh_javascript sunlight-highlight-javascript">// Access static members:
* Static members in the `$.ui.fancytree` namespace.
* This properties and methods can be accessed without instantiating a concrete
* Fancytree instance.
*
* @example
* // Access static members:
* var node = $.ui.fancytree.getNode(element);
* alert($.ui.fancytree.version);
* </pre>
*
* @mixin Fancytree_Static
*/
@@ -7941,11 +8026,14 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree,
/** @lends Fancytree_Static# */
{
/** @type {string} */
version: "2.33.0", // Set to semver by 'grunt release'
/** @type {string} */
/** Version number `"MAJOR.MINOR.PATCH"`
* @type {string} */
version: "2.34.0", // Set to semver by 'grunt release'
/** @type {string}
* @description `"production" for release builds` */
buildType: "production", // Set to 'production' by 'grunt build'
/** @type {int} */
/** @type {int}
* @description 0: silent .. 5: verbose (default: 3 for release builds). */
debugLevel: 3, // Set to 3 by 'grunt build'
// Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel
@@ -7954,9 +8042,15 @@ var uniqueId = $.fn.extend( {
_extensions: {},
// focusTree: null,
/** Expose class object as $.ui.fancytree._FancytreeClass */
/** Expose class object as `$.ui.fancytree._FancytreeClass`.
* Useful to extend `$.ui.fancytree._FancytreeClass.prototype`.
* @type {Fancytree}
*/
_FancytreeClass: Fancytree,
/** Expose class object as $.ui.fancytree._FancytreeNodeClass */
/** Expose class object as $.ui.fancytree._FancytreeNodeClass
* Useful to extend `$.ui.fancytree._FancytreeNodeClass.prototype`.
* @type {FancytreeNode}
*/
_FancytreeNodeClass: FancytreeNode,
/* Feature checks to provide backwards compatibility */
jquerySupports: {
@@ -7983,10 +8077,8 @@ var uniqueId = $.fn.extend( {
* @since 2.25
*/
createTree: function(el, opts) {
var tree = $(el)
.fancytree(opts)
.fancytree("getTree");
return tree;
var $tree = $(el).fancytree(opts);
return FT.getTree($tree);
},
/** Return a function that executes *fn* at most every *timeout* ms.
* @param {integer} timeout
@@ -8191,11 +8283,17 @@ var uniqueId = $.fn.extend( {
if (!el.length) {
el = $(orgEl).eq(0); // el was a selector: use first match
}
} else if (
el instanceof Element ||
el instanceof HTMLDocument
) {
el = $(el);
} else if (el instanceof $) {
el = el.eq(0); // el was a jQuery object: use the first DOM element
el = el.eq(0); // el was a jQuery object: use the first
} else if (el.originalEvent !== undefined) {
el = $(el.target); // el was an Event
}
// el is a jQuery object wit one element here
el = el.closest(":ui-fancytree");
widget = el.data("ui-fancytree") || el.data("fancytree"); // the latter is required by jQuery <= 1.8
return widget ? widget.tree : null;
@@ -8203,11 +8301,11 @@ var uniqueId = $.fn.extend( {
/** Return an option value that has a default, but may be overridden by a
* callback or a node instance attribute.
*
* Evaluation sequence:<br>
* Evaluation sequence:
*
* If tree.options.<optionName> is a callback that returns something, use that.<br>
* Else if node.<optionName> is defined, use that.<br>
* Else if tree.options.<optionName> is a value, use that.<br>
* If `tree.options.<optionName>` is a callback that returns something, use that.
* Else if `node.<optionName>` is defined, use that.
* Else if `tree.options.<optionName>` is a value, use that.
* Else use `defaultValue`.
*
* @param {string} optionName name of the option property (on node and tree)
@@ -8565,8 +8663,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
// To keep the global namespace clean, we wrap everything in a closure.
@@ -8602,7 +8700,7 @@ var uniqueId = $.fn.extend( {
// New member functions can be added to the `Fancytree` class.
// This function will be available for every tree instance:
//
// var tree = $("#tree").fancytree("getTree");
// var tree = $.ui.fancytree.getTree("#tree");
// tree.countSelected(false);
$.ui.fancytree._FancytreeClass.prototype.countSelected = function(topOnly) {
@@ -8685,7 +8783,7 @@ var uniqueId = $.fn.extend( {
// Every extension must be registered by a unique name.
name: "childcounter",
// Version information should be compliant with [semver](http://semver.org)
version: "2.33.0",
version: "2.34.0",
// Extension specific options and their defaults.
// This options will be available as `tree.options.childcounter.hideExpanded`
@@ -8796,8 +8894,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -9155,7 +9253,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "clones",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
@@ -9317,8 +9415,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
/*
@@ -10322,7 +10420,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "dnd5",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
@@ -10455,8 +10553,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -10748,7 +10846,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "edit",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
adjustWidthOfs: 4, // null: don't adjust input size to content
@@ -10859,8 +10957,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -11198,7 +11296,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "filter",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
autoApply: true, // Re-apply last filter if lazy data is loaded
@@ -11318,8 +11416,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -11502,7 +11600,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "glyph",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material'
@@ -11656,8 +11754,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -11774,7 +11872,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "gridnav",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
autofocusInput: false, // Focus first embedded input if node gets activated
@@ -11881,8 +11979,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -11911,7 +12009,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "multi",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
allowNoSelect: false, //
@@ -12013,8 +12111,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -12154,7 +12252,7 @@ var uniqueId = $.fn.extend( {
/**
* [ext-persist] Remove persistence data of the given type(s).
* Called like
* $("#tree").fancytree("getTree").clearCookies("active expanded focus selected");
* $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected");
*
* @alias Fancytree#clearPersistData
* @requires jquery.fancytree.persist.js
@@ -12191,7 +12289,7 @@ var uniqueId = $.fn.extend( {
* [ext-persist] Return persistence information from cookies
*
* Called like
* $("#tree").fancytree("getTree").getPersistData();
* $.ui.fancytree.getTree("#tree").getPersistData();
*
* @alias Fancytree#getPersistData
* @requires jquery.fancytree.persist.js
@@ -12214,7 +12312,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "persist",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
cookieDelimiter: "~",
@@ -12507,8 +12605,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -12591,7 +12689,7 @@ var uniqueId = $.fn.extend( {
$.ui.fancytree.registerExtension({
name: "table",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
@@ -13057,8 +13155,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -13081,7 +13179,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "themeroller",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
activeClass: "ui-state-active", // Class added to active node
@@ -13177,8 +13275,8 @@ var uniqueId = $.fn.extend( {
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
*/
(function(factory) {
@@ -13308,7 +13406,7 @@ var uniqueId = $.fn.extend( {
*/
$.ui.fancytree.registerExtension({
name: "wide",
version: "2.33.0",
version: "2.34.0",
// Default options for this extension.
options: {
iconWidth: null, // Adjust this if @fancy-icon-width != "16px"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -13,8 +13,8 @@
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version 2.33.0
* @date 2019-10-29T08:00:07Z
* @version 2.34.0
* @date 2019-12-26T14:16:19Z
******************************************************************************/
/*------------------------------------------------------------------------------
* Helpers

3
libraries/split.min.js vendored Normal file

File diff suppressed because one or more lines are too long

568
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.38.2",
"version": "0.39.4",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -27,23 +27,23 @@
"commonmark": "0.29.0",
"cookie-parser": "1.4.4",
"csurf": "1.10.0",
"dayjs": "1.8.17",
"dayjs": "1.8.18",
"debug": "4.1.1",
"ejs": "2.7.4",
"electron-debug": "3.0.1",
"electron-dl": "1.14.0",
"electron-dl": "2.0.0",
"electron-find": "1.0.6",
"electron-spellchecker": "2.2.1",
"electron-window-state": "5.0.3",
"express": "4.17.1",
"express-session": "1.17.0",
"file-type": "12.4.0",
"file-type": "12.4.2",
"fs-extra": "8.1.0",
"helmet": "3.21.2",
"html": "1.0.0",
"html2plaintext": "2.1.2",
"http-proxy-agent": "2.1.0",
"https-proxy-agent": "3.0.1",
"http-proxy-agent": "3.0.0",
"https-proxy-agent": "4.0.0",
"image-type": "4.1.0",
"imagemin": "7.0.1",
"imagemin-giflossy": "5.1.10",
@@ -63,7 +63,7 @@
"rimraf": "3.0.0",
"sanitize-filename": "1.6.3",
"sax": "1.2.4",
"semver": "6.3.0",
"semver": "7.1.1",
"serve-favicon": "2.5.0",
"session-file-store": "1.3.1",
"simple-node-logger": "18.12.23",
@@ -74,15 +74,17 @@
"turndown": "5.0.3",
"turndown-plugin-gfm": "1.0.2",
"unescape": "1.0.1",
"ws": "7.2.0"
"ws": "7.2.1"
},
"devDependencies": {
"electron": "6.0.12",
"electron-builder": "22.1.0",
"electron-installer-debian": "2.0.1",
"electron-builder": "21.2.0",
"electron-packager": "14.1.1",
"electron-rebuild": "1.8.8",
"jsdoc": "3.6.3",
"lorem-ipsum": "2.0.3"
},
"optionalDependencies": {
"electron-installer-debian": "2.0.1"
}
}

View File

@@ -525,7 +525,7 @@ class Note extends Entity {
const attributes = await this.loadOwnedAttributesToCache();
for (const attribute of attributes) {
if (attribute.type === type && (value === undefined || value === attribute.value)) {
if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
attribute.isDeleted = true;
await attribute.save();

View File

@@ -31,6 +31,9 @@ import dateNoteService from './services/date_notes.js';
import sidebarService from './services/sidebar.js';
import importService from './services/import.js';
import keyboardActionService from "./services/keyboard_actions.js";
import splitService from "./services/split.js";
import optionService from "./services/options.js";
import noteContentRenderer from "./services/note_content_renderer.js";
window.glob.isDesktop = utils.isDesktop;
window.glob.isMobile = utils.isMobile;
@@ -39,6 +42,20 @@ window.glob.isMobile = utils.isMobile;
window.glob.getActiveNode = treeService.getActiveNode;
window.glob.getHeaders = server.getHeaders;
window.glob.showAddLinkDialog = () => import('./dialogs/add_link.js').then(d => d.showDialog());
window.glob.showIncludeNoteDialog = cb => import('./dialogs/include_note.js').then(d => d.showDialog(cb));
window.glob.loadIncludedNote = async (noteId, el) => {
const note = await treeCache.getNote(noteId);
if (note) {
$(el).empty().append($("<h3>").append(await linkService.createNoteLink(note.noteId, {
showTooltip: false
})));
const {renderedContent} = await noteContentRenderer.getRenderedContent(note);
$(el).append(renderedContent);
}
};
// this is required by CKEditor when uploading images
window.glob.noteChanged = noteDetailService.noteChanged;
window.glob.refreshTree = treeService.reload;
@@ -142,7 +159,10 @@ async function printActiveNote() {
$tabContext.$tabContent.find('.note-detail-component:visible').printThis({
header: $("<h2>").text($tabContext.note && $tabContext.note.title).prop('outerHTML') ,
importCSS: false,
loadCSS: "libraries/codemirror/codemirror.css",
loadCSS: [
"libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css"
],
debug: true
});
}
@@ -176,4 +196,34 @@ noteAutocompleteService.init();
if (utils.isElectron()) {
import("./services/spell_check.js").then(spellCheckService => spellCheckService.initSpellCheck());
}
}
optionService.waitForOptions().then(options => {
if (utils.isElectron() && !options.is('nativeTitleBarVisible')) {
$("#title-bar-buttons").show();
$("#minimize-btn").on('click', () => {
$("#minimize-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().minimize();
});
$("#maximize-btn").on('click', () => {
$("#maximize-btn").trigger('blur');
const {remote} = require('electron');
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
if (focusedWindow.isMaximized()) {
focusedWindow.unmaximize();
} else {
focusedWindow.maximize();
}
});
$("#close-btn").on('click', () => {
$("#close-btn").trigger('blur');
const {remote} = require('electron');
remote.BrowserWindow.getFocusedWindow().close();
});
}
});

View File

@@ -0,0 +1,40 @@
import treeUtils from '../services/tree_utils.js';
import noteAutocompleteService from '../services/note_autocomplete.js';
import utils from "../services/utils.js";
const $dialog = $("#include-note-dialog");
const $form = $("#include-note-form");
const $autoComplete = $("#include-note-autocomplete");
let callback = null;
export async function showDialog(cb) {
callback = cb;
utils.closeActiveDialog();
glob.activeDialog = $dialog;
$autoComplete.val('');
$dialog.modal();
noteAutocompleteService.initNoteAutocomplete($autoComplete, { hideGoToSelectedNoteButton: true });
noteAutocompleteService.showRecentNotes($autoComplete);
}
$form.on('submit', () => {
const notePath = $autoComplete.getSelectedPath();
if (notePath) {
$dialog.modal('hide');
if (callback) {
callback(treeUtils.getNoteIdFromNotePath(notePath));
}
}
else {
console.error("No noteId to include.");
}
return false;
});

View File

@@ -19,12 +19,13 @@ const TPL = `
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div>
<div class="col-4">
<label for="one-tab-display-select">If there's only one tab, then...</label>
<select class="form-control" id="one-tab-display-select">
<option value="show">show the tab bar</option>
<option value="hide">hide the tab bar</option>
<label for="native-title-bar-select">Native title bar (requires app restart)</label>
<select class="form-control" id="native-title-bar-select">
<option value="show">enabled</option>
<option value="hide">disabled</option>
</select>
</div>
</div>
@@ -69,37 +70,6 @@ const TPL = `
</div>
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
<h4>Left pane sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="left-pane-min-width">Left pane minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Left pane width percent of window size</label>
<div class="input-group">
<input type="number" class="form-control" id="left-pane-width-percent" min="0" max="99" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Left pane width is calculated from the percent of window size, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width left pane, set minimum width to the desired width and set percent to 0.</p>
</form>`;
export default class ApperanceOptions {
@@ -108,9 +78,7 @@ export default class ApperanceOptions {
this.$themeSelect = $("#theme-select");
this.$zoomFactorSelect = $("#zoom-factor-select");
this.$oneTabDisplaySelect = $("#one-tab-display-select");
this.$leftPaneMinWidth = $("#left-pane-min-width");
this.$leftPaneWidthPercent = $("#left-pane-width-percent");
this.$nativeTitleBarSelect = $("#native-title-bar-select");
this.$mainFontSize = $("#main-font-size");
this.$treeFontSize = $("#tree-font-size");
this.$detailFontSize = $("#detail-font-size");
@@ -141,23 +109,10 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.on('change', () => { zoomService.setZoomFactorAndSave(this.$zoomFactorSelect.val()); });
this.$oneTabDisplaySelect.on('change', () => {
const hideTabRowForOneTab = this.$oneTabDisplaySelect.val() === 'hide' ? 'true' : 'false';
this.$nativeTitleBarSelect.on('change', () => {
const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
server.put('options/hideTabRowForOneTab/' + hideTabRowForOneTab)
.then(optionsService.reloadOptions);
});
this.$leftPaneMinWidth.on('change', async () => {
await server.put('options/leftPaneMinWidth/' + this.$leftPaneMinWidth.val());
this.resizeLeftPanel();
});
this.$leftPaneWidthPercent.on('change', async () => {
await server.put('options/leftPaneWidthPercent/' + this.$leftPaneWidthPercent.val());
this.resizeLeftPanel();
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible);
});
this.$mainFontSize.on('change', async () => {
@@ -204,24 +159,13 @@ export default class ApperanceOptions {
this.$zoomFactorSelect.prop('disabled', true);
}
this.$oneTabDisplaySelect.val(options.hideTabRowForOneTab === 'true' ? 'hide' : 'show');
this.$leftPaneMinWidth.val(options.leftPaneMinWidth);
this.$leftPaneWidthPercent.val(options.leftPaneWidthPercent);
this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide');
this.$mainFontSize.val(options.mainFontSize);
this.$treeFontSize.val(options.treeFontSize);
this.$detailFontSize.val(options.detailFontSize);
}
resizeLeftPanel() {
const leftPanePercent = parseInt(this.$leftPaneWidthPercent.val());
const rightPanePercent = 100 - leftPanePercent;
const leftPaneMinWidth = this.$leftPaneMinWidth.val();
this.$container.css("grid-template-columns", `minmax(${leftPaneMinWidth}px, ${leftPanePercent}fr) ${rightPanePercent}fr`);
}
applyFontSizes() {
this.$body.get(0).style.setProperty("--main-font-size", this.$mainFontSize.val() + "%");
this.$body.get(0).style.setProperty("--tree-font-size", this.$treeFontSize.val() + "%");

View File

@@ -3,45 +3,6 @@ import server from "../../services/server.js";
import optionsService from "../../services/options.js";
const TPL = `
<h4>Show sidebar in new tab</h4>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="show-sidebar-in-new-tab">
<label class="form-check-label" for="show-sidebar-in-new-tab">Show sidebar in new tab</label>
</div>
<br>
<h4>Sidebar sizing</h4>
<div class="form-group row">
<div class="col-6">
<label for="sidebar-min-width">Sidebar minimum width (in pixels)</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-min-width" min="100" max="2000" step="1"/>
<div class="input-group-append">
<span class="input-group-text">px</span>
</div>
</div>
</div>
<div class="col-6">
<label for="left-pane-min-width">Sidebar width percent of the detail pane</label>
<div class="input-group">
<input type="number" class="form-control" id="sidebar-width-percent" min="0" max="70" step="1"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Sidebar width is calculated from the percent of the detail pane, if this is smaller than minimum width, then minimum width is used. If you want to have fixed width sidebar, set minimum width to the desired width and set percent to 0.</p>
<h4>Widgets</h4>
<div id="widgets-configuration" class="row">
@@ -58,48 +19,15 @@ export default class SidebarOptions {
constructor() {
$("#options-sidebar").html(TPL);
this.$sidebarMinWidth = $("#sidebar-min-width");
this.$sidebarWidthPercent = $("#sidebar-width-percent");
this.$showSidebarInNewTab = $("#show-sidebar-in-new-tab");
this.$widgetsConfiguration = $("#widgets-configuration");
this.$widgetsEnabled = $("#widgets-enabled");
this.$widgetsDisabled = $("#widgets-disabled");
this.$sidebarMinWidth.on('change', async () => {
await server.put('options/sidebarMinWidth/' + this.$sidebarMinWidth.val());
this.resizeSidebar();
});
this.$sidebarWidthPercent.on('change', async () => {
await server.put('options/sidebarWidthPercent/' + this.$sidebarWidthPercent.val());
this.resizeSidebar();
});
this.$showSidebarInNewTab.on('change', async () => {
const flag = this.$showSidebarInNewTab.is(":checked") ? 'true' : 'false';
await server.put('options/showSidebarInNewTab/' + flag);
optionsService.reloadOptions();
});
}
async optionsLoaded(options) {
this.$widgetsEnabled.empty();
this.$widgetsDisabled.empty();
this.$sidebarMinWidth.val(options.sidebarMinWidth);
this.$sidebarWidthPercent.val(options.sidebarWidthPercent);
if (options.showSidebarInNewTab === 'true') {
this.$showSidebarInNewTab.attr("checked", "checked");
}
else {
this.$showSidebarInNewTab.removeAttr("checked");
}
const widgets = [
{name: 'attributes', title: 'Attributes'},
{name: 'linkMap', title: 'Link map'},
@@ -188,19 +116,4 @@ export default class SidebarOptions {
return null;
}
}
resizeSidebar() {
const sidebarWidthPercent = parseInt(this.$sidebarWidthPercent.val());
const sidebarMinWidth = this.$sidebarMinWidth.val();
// need to find them dynamically since they change
const $sidebar = $(".note-detail-sidebar");
const $content = $(".note-detail-content");
$sidebar.css("width", sidebarWidthPercent + '%');
$sidebar.css("min-width", sidebarMinWidth + 'px');
$content.css("width", (100 - sidebarWidthPercent) + '%');
}
}

View File

@@ -12,7 +12,7 @@ const TPL = `
<div class="form-group">
<label for="sync-server-timeout">Sync timeout (milliseconds)</label>
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number">
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number" style="text-align: left;">
</div>
<div class="form-group">

View File

@@ -44,7 +44,10 @@ export async function showDialog() {
const note = await treeCache.getNote(change.noteId);
const notePath = await treeService.getSomeNotePath(note);
noteLink = await linkService.createNoteLinkWithPath(notePath, change.title);
noteLink = await linkService.createNoteLink(notePath, {
title: change.title,
showNotePath: true
});
}
changesListEl.append($('<li>')

View File

@@ -9,6 +9,7 @@ import noteDetailService from "./note_detail.js";
import keyboardActionService from "./keyboard_actions.js";
import hoistedNoteService from "./hoisted_note.js";
import treeCache from "./tree_cache.js";
import server from "./server.js";
const NOTE_REVISIONS = "../dialogs/note_revisions.js";
const OPTIONS = "../dialogs/options.js";
@@ -111,10 +112,21 @@ function registerEntrypoints() {
keyboardActionService.setGlobalActionHandler("ForwardInNoteHistory", window.history.forward);
}
let zenModeActive = false;
// hide (toggle) everything except for the note content for zen mode
const toggleZenMode = () => {
$(".hide-in-zen-mode").toggle();
$("#container").toggleClass("zen-mode");
if (!zenModeActive) {
$(".hide-in-zen-mode,.gutter").addClass("hidden-by-zen-mode");
$("#container").addClass("zen-mode");
zenModeActive = true;
}
else {
// not hiding / showing explicitly since element might be hidden also for other reasons
$(".hide-in-zen-mode,.gutter").removeClass("hidden-by-zen-mode");
$("#container").removeClass("zen-mode");
zenModeActive = false;
}
};
$("#toggle-zen-mode-button").on('click', toggleZenMode);
@@ -232,16 +244,19 @@ function registerEntrypoints() {
keyboardActionService.setGlobalActionHandler("CreateNoteIntoDayNote", async () => {
const todayNote = await dateNoteService.getTodayNote();
const notePath = await treeService.getSomeNotePath(todayNote);
const node = await treeService.expandToNote(notePath);
const {note} = await treeService.createNote(node, todayNote.noteId, 'into', {
type: "text",
isProtected: node.data.isProtected
const {note} = await server.post(`notes/${todayNote.noteId}/children?target=into`, {
title: 'new note',
content: '',
type: 'text',
isProtected: todayNote.isProtected
});
await treeService.expandToNote(note.noteId);
await noteDetailService.openInTab(note.noteId, true);
noteDetailService.focusAndSelectTitle();
});
keyboardActionService.setGlobalActionHandler("EditBranchPrefix", async () => {

View File

@@ -58,13 +58,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
};
/**
* Activates newly created note. Compared to this.activateNote() also refreshes tree.
* Activates newly created note. Compared to this.activateNote() also makes sure that frontend has been fully synced.
*
* @param {string} notePath (or noteId)
* @return {Promise<void>}
*/
this.activateNewNote = async notePath => {
await treeService.reload();
await ws.waitForMaxKnownSyncId();
await treeService.activateNote(notePath, noteDetailService.focusAndSelectTitle);
};
@@ -125,14 +125,14 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
/**
* Executes given anonymous function on the server.
* Executes given anonymous function on the backend.
* Internally this serializes the anonymous function into string and sends it to backend via AJAX.
*
* @param {string} script - script to be executed on the backend
* @param {Array.<?>} params - list of parameters to the anonymous function to be send to backend
* @return {Promise<*>} return value of the executed function on the backend
*/
this.runOnServer = async (script, params = []) => {
this.runOnBackend = async (script, params = []) => {
if (typeof script === "function") {
script = script.toString();
}
@@ -159,6 +159,12 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, tabConte
}
};
/**
* @deprecated new name of this API call is runOnBackend so use that
* @method
*/
this.runOnServer = this.runOnBackend;
/**
* This is a powerful search method - you can search by attributes and their values, e.g.:
* "@dateModified =* MONTH AND @log". See full documentation for all options at: https://github.com/zadam/trilium/wiki/Search

View File

@@ -6,15 +6,14 @@ import noteDetailService from "./note_detail.js";
function getNotePathFromUrl(url) {
const notePathMatch = /#(root[A-Za-z0-9/]*)$/.exec(url);
if (notePathMatch === null) {
return null;
}
else {
return notePathMatch[1];
}
return notePathMatch === null ? null : notePathMatch[1];
}
async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
async function createNoteLink(notePath, options = {}) {
let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
if (!noteTitle) {
const {noteId, parentNoteId} = treeUtils.getNoteIdAndParentIdFromNotePath(notePath);
@@ -27,30 +26,28 @@ async function createNoteLink(notePath, noteTitle = null, tooltip = true) {
}).attr('data-action', 'note')
.attr('data-note-path', notePath);
if (!tooltip) {
if (!showTooltip) {
$noteLink.addClass("no-tooltip-preview");
}
return $noteLink;
}
const $container = $("<span>").append($noteLink);
async function createNoteLinkWithPath(notePath, noteTitle = null) {
const $link = await createNoteLink(notePath, noteTitle);
if (showNotePath) {
notePath = await treeService.resolveNotePath(notePath);
const $res = $("<span>").append($link);
if (notePath) {
const noteIds = notePath.split("/");
noteIds.pop(); // remove last element
if (notePath.includes("/")) {
const noteIds = notePath.split("/");
noteIds.pop(); // remove last element
const parentNotePath = noteIds.join("/").trim();
const parentNotePath = noteIds.join("/").trim();
if (parentNotePath) {
$res.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
if (parentNotePath) {
$container.append($("<small>").text(" (" + await treeUtils.getNotePathTitle(parentNotePath) + ")"));
}
}
}
return $res;
return $container;
}
function getNotePathFromLink($link) {
@@ -66,7 +63,10 @@ function getNotePathFromLink($link) {
}
function goToLink(e) {
const $link = $(e.target);
e.preventDefault();
e.stopPropagation();
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -89,9 +89,6 @@ function goToLink(e) {
}
}
e.preventDefault();
e.stopPropagation();
return true;
}
@@ -118,7 +115,7 @@ function addTextToEditor(text) {
}
function newTabContextMenu(e) {
const $link = $(e.target);
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
@@ -142,22 +139,27 @@ function newTabContextMenu(e) {
});
}
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
// 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('mousedown', "a[data-action='note']", goToLink);
$(document).on('mousedown', 'div.popover-content a, div.ui-tooltip-content a', goToLink);
$(document).on('dblclick', '.note-detail-text a', goToLink);
$(document).on('mousedown', '.note-detail-text a', function (e) {
const notePath = getNotePathFromLink($(e.target));
if (notePath && ((e.which === 1 && e.ctrlKey) || e.which === 2)) {
const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link);
if ((e.which === 1 && e.ctrlKey) || e.which === 2) {
// if it's a ctrl-click, then we open on new tab, otherwise normal flow (CKEditor opens link-editing dialog)
e.preventDefault();
noteDetailService.loadNoteDetail(notePath, { newTab: true });
if (notePath) {
noteDetailService.loadNoteDetail(notePath, {newTab: true});
}
else {
const address = $link.attr('href');
window.open(address, '_blank');
}
return true;
}
@@ -166,24 +168,20 @@ $(document).on('mousedown', '.note-detail-text a', function (e) {
$(document).on('mousedown', '.note-detail-book a', goToLink);
$(document).on('mousedown', '.note-detail-render a', goToLink);
$(document).on('mousedown', '.note-detail-text.ck-read-only a', goToLink);
$(document).on('mousedown', 'span.ck-button__label', e => {
// this is a link preview dialog from CKEditor link editing
// for some reason clicked element is span
const url = $(e.target).text();
const notePath = getNotePathFromUrl(url);
if (notePath) {
treeService.activateNote(notePath);
e.preventDefault();
}
$(document).on('mousedown', 'a.ck-link-actions__preview', goToLink);
$(document).on('click', 'a.ck-link-actions__preview', e => {
e.preventDefault();
e.stopPropagation();
});
$(document).on('contextmenu', 'a.ck-link-actions__preview', newTabContextMenu);
$(document).on('contextmenu', '.note-detail-text a', newTabContextMenu);
$(document).on('contextmenu', "a[data-action='note']", newTabContextMenu);
$(document).on('contextmenu', ".note-detail-render a", newTabContextMenu);
export default {
getNotePathFromUrl,
createNoteLink,
createNoteLinkWithPath,
addLinkToEditor,
addTextToEditor,
goToLink

View File

@@ -88,7 +88,7 @@ export default class LinkMap {
.addClass("note-box")
.prop("id", noteBoxId);
linkService.createNoteLink(noteId, note.title).then($link => {
linkService.createNoteLink(noteId, {title: note.title}).then($link => {
$link.on('click', e => {
try {
$link.tooltip('dispose');

View File

@@ -0,0 +1,107 @@
import server from "./server.js";
import utils from "./utils.js";
import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
async function getRenderedContent(note) {
const type = getRenderingType(note);
let rendered;
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
rendered = "";
}
else {
rendered = $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
rendered = "";
}
rendered = $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
rendered = $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
rendered = $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
rendered = $el;
}
else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
rendered = $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
rendered = "<em>Content of this note cannot be displayed in the book format</em>";
}
return {
renderedContent: rendered,
type
};
}
function getRenderingType(note) {
let type = note.type;
if (note.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
export default {
getRenderedContent
};

View File

@@ -174,7 +174,11 @@ async function showTab(tabId) {
if (newActiveTabContext && newActiveTabContext.notePath) {
const newActiveNode = await treeService.getNodeFromPath(newActiveTabContext.notePath);
if (newActiveNode && newActiveNode.isVisible()) {
if (newActiveNode) {
if (!newActiveNode.isVisible()) {
await treeService.expandToNote(newActiveTabContext.notePath);
}
newActiveNode.setActive(true, {noEvents: true});
}
}

View File

@@ -1,10 +1,6 @@
import server from "./server.js";
import linkService from "./link.js";
import utils from "./utils.js";
import treeCache from "./tree_cache.js";
import renderService from "./render.js";
import protectedSessionHolder from "./protected_session_holder.js";
import protectedSessionService from "./protected_session.js";
import noteContentRenderer from "./note_content_renderer.js";
const MIN_ZOOM_LEVEL = 1;
const MAX_ZOOM_LEVEL = 6;
@@ -47,6 +43,7 @@ class NoteDetailBook {
this.$zoomInButton = this.$component.find('.book-zoom-in-button');
this.$zoomOutButton = this.$component.find('.book-zoom-out-button');
this.$expandChildrenButton = this.$component.find('.expand-children-button');
this.$help = this.$component.find('.note-detail-book-help');
this.$zoomInButton.on('click', () => this.setZoom(this.zoomLevel - 1));
this.$zoomOutButton.on('click', () => this.setZoom(this.zoomLevel + 1));
@@ -109,6 +106,7 @@ class NoteDetailBook {
async render() {
this.$content.empty();
this.$help.hide();
if (this.isAutoBook()) {
const $addTextLink = $('<a href="javascript:">here</a>').on('click', () => {
@@ -128,19 +126,21 @@ class NoteDetailBook {
}
async renderIntoElement(note, $container) {
for (const childNote of await note.getChildNotes()) {
const type = this.getRenderingType(childNote);
const childNotes = await note.getChildNotes();
for (const childNote of childNotes) {
const childNotePath = this.ctx.notePath + '/' + childNote.noteId;
const {type, renderedContent} = await noteContentRenderer.getRenderedContent(childNote);
const $card = $('<div class="note-book-card">')
.attr('data-note-id', childNote.noteId)
.css("flex-basis", ZOOMS[this.zoomLevel].width)
.addClass("type-" + type)
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, null, false)))
.append($('<h5 class="note-book-title">').append(await linkService.createNoteLink(childNotePath, {showTooltip: false})))
.append($('<div class="note-book-content">')
.css("max-height", ZOOMS[this.zoomLevel].height)
.append(await this.getNoteContent(type, childNote)));
.append(renderedContent));
const childCount = childNote.getChildNoteIds().length;
@@ -156,79 +156,9 @@ class NoteDetailBook {
$container.append($card);
}
}
async getNoteContent(type, note) {
if (type === 'text') {
const fullNote = await server.get('notes/' + note.noteId);
const $content = $("<div>").html(fullNote.content);
if (utils.isHtmlEmpty(fullNote.content)) {
return "";
}
else {
return $content;
}
}
else if (type === 'code') {
const fullNote = await server.get('notes/' + note.noteId);
if (fullNote.content.trim() === "") {
return "";
}
return $("<pre>").text(fullNote.content);
}
else if (type === 'image') {
return $("<img>").attr("src", `api/images/${note.noteId}/${note.title}`);
}
else if (type === 'file') {
function getFileUrl() {
return utils.getUrlForDownload("api/notes/" + note.noteId + "/download");
}
const $downloadButton = $('<button class="file-download btn btn-primary" type="button">Download</button>');
const $openButton = $('<button class="file-open btn btn-primary" type="button">Open</button>');
$downloadButton.on('click', () => utils.download(getFileUrl()));
$openButton.on('click', () => {
if (utils.isElectron()) {
const open = require("open");
open(getFileUrl(), {url: true});
}
else {
window.location.href = getFileUrl();
}
});
// open doesn't work for protected notes since it works through browser which isn't in protected session
$openButton.toggle(!note.isProtected);
return $('<div>')
.append($downloadButton)
.append(' &nbsp; ')
.append($openButton);
}
else if (type === 'render') {
const $el = $('<div>');
await renderService.render(note, $el, this.ctx);
return $el;
}
else if (type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession);
return $("<div>")
.append("<div>This note is protected and to access it you need to enter password.</div>")
.append("<br/>")
.append($button);
}
else {
return "<em>Content of this note cannot be displayed in the book format</em>";
if (childNotes.length === 0) {
this.$help.show();
}
}
@@ -256,21 +186,6 @@ class NoteDetailBook {
}
}
getRenderingType(childNote) {
let type = childNote.type;
if (childNote.isProtected) {
if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.touchProtectedSession();
}
else {
type = 'protected-session';
}
}
return type;
}
getContent() {
// for auto-book cases when renaming title there should be content
return "";

View File

@@ -39,8 +39,11 @@ class NoteDetailFile {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'notes/' + this.ctx.note.noteId + '/file',

View File

@@ -48,8 +48,11 @@ class NoteDetailImage {
});
this.$uploadNewRevisionInput.on('change', async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val('');
const formData = new FormData();
formData.append('upload', this.$uploadNewRevisionInput[0].files[0]);
formData.append('upload', fileToUpload);
const result = await $.ajax({
url: baseApiUrl + 'images/' + this.ctx.note.noteId,

View File

@@ -129,9 +129,10 @@ class NoteDetailRelationMap {
return;
}
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children`, {
const {note} = await server.post(`notes/${this.ctx.note.noteId}/children?target=into`, {
title,
target: 'into'
content: '',
type: 'text'
});
toastService.showMessage("Click on canvas to place new note");
@@ -493,7 +494,7 @@ class NoteDetailRelationMap {
}
async createNoteBox(noteId, title, x, y) {
const $link = await linkService.createNoteLink(noteId, title);
const $link = await linkService.createNoteLink(noteId, {title});
$link.mousedown(e => {
console.log(e);

View File

@@ -3,6 +3,8 @@ import treeService from './tree.js';
import noteAutocompleteService from './note_autocomplete.js';
import mimeTypesService from './mime_types.js';
const ENABLE_INSPECTOR = false;
const mentionSetup = {
feeds: [
{
@@ -61,7 +63,7 @@ class NoteDetailText {
else {
window.open(src, '_blank');
}
})
});
}
async render() {
@@ -94,6 +96,11 @@ class NoteDetailText {
}
});
if (glob.isDev && ENABLE_INSPECTOR) {
await import('../../libraries/ckeditor/inspector.js');
CKEditorInspector.attach(this.textEditor);
}
this.onNoteChange(() => this.ctx.noteChanged());
}
}
@@ -109,15 +116,19 @@ class NoteDetailText {
}
getContent() {
let content = this.textEditor.getData();
const content = this.textEditor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</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 this.isContentEmpty(content) ? '' : content;
}
return content;
isContentEmpty(content) {
content = content.toLowerCase();
return jQuery(content).text().trim() === ''
&& !content.includes("<img")
&& !content.includes("<section")
}
async isReadOnly() {

View File

@@ -1,6 +1,47 @@
import bundleService from "./bundle.js";
import ws from "./ws.js";
import optionsService from "./options.js";
import splitService from "./split.js";
import optionService from "./options.js";
import server from "./server.js";
import noteDetailService from "./note_detail.js";
const $sidebar = $("#right-pane");
const $sidebarContainer = $('#sidebar-container');
const $showSidebarButton = $("#show-sidebar-button");
const $hideSidebarButton = $("#hide-sidebar-button");
optionService.waitForOptions().then(options => toggleSidebar(options.is('rightPaneVisible')));
function toggleSidebar(show) {
$sidebar.toggle(show);
$showSidebarButton.toggle(!show);
$hideSidebarButton.toggle(show);
if (show) {
splitService.setupSplitWithSidebar();
}
else {
splitService.setupSplitWithoutSidebar();
}
}
$hideSidebarButton.on('click', () => {
toggleSidebar(false);
server.put('options/rightPaneVisible/false');
});
$showSidebarButton.on('click', async () => {
toggleSidebar(true);
await server.put('options/rightPaneVisible/true');
const {sidebar} = noteDetailService.getActiveTabContext();
await sidebar.noteLoaded();
sidebar.show();
});
class Sidebar {
/**
@@ -14,30 +55,18 @@ class Sidebar {
widgets: []
}, state);
this.widgets = [];
this.$sidebar = ctx.$tabContent.find(".note-detail-sidebar");
this.$widgetContainer = this.$sidebar.find(".note-detail-widget-container");
this.$showSideBarButton = this.ctx.$tabContent.find(".show-sidebar-button");
this.$hideSidebarButton = this.$sidebar.find(".hide-sidebar-button");
this.$hideSidebarButton.on('click', () => {
this.$sidebar.hide();
this.$showSideBarButton.show();
this.ctx.stateChanged();
});
this.$widgetContainer = $sidebar.find(`.sidebar-widget-container[data-tab-id=${this.ctx.tabId}]`);
this.$showSideBarButton.on('click', () => {
this.$sidebar.show();
this.$showSideBarButton.hide();
this.ctx.stateChanged();
this.noteLoaded();
});
if (this.$widgetContainer.length === 0) {
this.$widgetContainer = $(`<div class="sidebar-widget-container">`).attr('data-tab-id', this.ctx.tabId);
this.$showSideBarButton.toggle(!state.visible);
this.$sidebar.toggle(state.visible);
$sidebarContainer.append(this.$widgetContainer);
}
}
isVisible() {
return this.$sidebar.css("display") !== "none";
return $sidebar.css("display") !== "none";
}
getSidebarState() {
@@ -91,6 +120,12 @@ class Sidebar {
this.renderWidgets(widgets);
}
show() {
$sidebarContainer.find('.sidebar-widget-container').each((i, el) => {
$(el).toggle($(el).attr('data-tab-id') === this.ctx.tabId);
});
}
// it's important that this method is sync so that the whole render-update is atomic
// otherwise we could get race issues (doubled widgets etc.)
renderWidgets(widgets) {
@@ -119,6 +154,12 @@ class Sidebar {
this.$widgetContainer.empty().append(...widgetsToAppend);
}
remove() {
if (this.$widgetContainer) {
this.$widgetContainer.remove();
}
}
eventReceived(name, data) {
for (const widget of this.widgets) {
if (widget.eventReceived) {

View File

@@ -0,0 +1,51 @@
import server from "./server.js";
import optionService from "./options.js";
let instance;
async function getPaneWidths() {
const options = await optionService.waitForOptions();
return {
leftPaneWidth: options.getInt('leftPaneWidth'),
rightPaneWidth: options.getInt('rightPaneWidth')
};
}
async function setupSplitWithSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth, rightPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth - rightPaneWidth, rightPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
server.put('options/rightPaneWidth/' + Math.round(sizes[2]));
}
});
}
async function setupSplitWithoutSidebar() {
if (instance) {
instance.destroy();
}
const {leftPaneWidth} = await getPaneWidths();
instance = Split(['#left-pane', '#center-pane'], {
sizes: [leftPaneWidth, 100 - leftPaneWidth],
gutterSize: 5,
onDragEnd: sizes => {
server.put('options/leftPaneWidth/' + Math.round(sizes[0]));
}
});
}
export default {
setupSplitWithSidebar,
setupSplitWithoutSidebar
};

View File

@@ -148,6 +148,10 @@ class TabContext {
this.setCurrentNotePathToHash();
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
this.noteChangeDisabled = true;
try {
@@ -158,9 +162,6 @@ class TabContext {
this.noteChangeDisabled = false;
}
// after loading new note make sure editor is scrolled to the top
this.getComponent().scrollToTop();
this.setTitleBar();
this.cleanup(); // esp. on windows autocomplete is not getting closed automatically
@@ -181,11 +182,10 @@ class TabContext {
this.noteType.update();
}
if (this.sidebar) {
this.sidebar.noteLoaded(); // load async
}
bundleService.executeRelationBundles(this.note, 'runOnNoteView', this);
// after loading new note make sure editor is scrolled to the top
this.getComponent().scrollToTop();
}
async show() {
@@ -204,6 +204,10 @@ class TabContext {
this.$tabContent.show();
if (this.sidebar) {
this.sidebar.show();
}
this.setCurrentNotePathToHash();
this.setTitleBar();
}
@@ -285,6 +289,10 @@ class TabContext {
}
getComponent() {
if (!this.components[this.type]) {
throw new Error("Could not find component for type: " + this.type);
}
return this.components[this.type];
}
@@ -371,7 +379,7 @@ class TabContext {
async addPath(notePath, isCurrent) {
const title = await treeUtils.getNotePathTitle(notePath);
const noteLink = await linkService.createNoteLink(notePath, title);
const noteLink = await linkService.createNoteLink(notePath, {title});
noteLink
.addClass("no-tooltip-preview")
@@ -411,6 +419,16 @@ class TabContext {
await this.addPath(notePath, isCurrent);
}
const cloneLink = $("<div>")
.addClass("dropdown-item")
.append(
$('<button class="btn btn-sm">')
.text('Clone note to new location...')
.on('click', () => import("../dialogs/clone_to.js").then(d => d.showDialog([this.note.noteId])))
);
this.$notePathList.append(cloneLink);
}
}
@@ -422,6 +440,10 @@ class TabContext {
await this.saveNoteIfChanged();
this.$tabContent.remove();
}
if (this.sidebar) {
this.sidebar.remove();
}
}
cleanup() {

File diff suppressed because one or more lines are too long

View File

@@ -127,16 +127,16 @@ async function getNodeFromPath(notePath, expand = false, expandOpts = {}) {
// we expand only after hoisted note since before then nodes are not actually present in the tree
if (parentNode) {
checkFolderStatus(parentNode);
if (!parentNode.isLoaded()) {
await parentNode.load();
}
if (expand) {
parentNode.setExpanded(true, expandOpts);
await parentNode.setExpanded(true, expandOpts);
}
await checkFolderStatus(parentNode);
let foundChildNode = findChildNode(parentNode, childNoteId);
if (!foundChildNode) { // note might be recently created so we'll force reload and try again
@@ -191,9 +191,7 @@ async function activateNote(notePath, noteLoadedListener) {
noteDetailService.addDetailLoadedListener(node.data.noteId, noteLoadedListener);
}
// we use noFocus because when we reload the tree because of background changes
// we don't want the reload event to steal focus from whatever was focused before
await node.setActive(true, { noFocus: true });
await node.setActive(true);
clearSelectedNodes();
@@ -318,6 +316,8 @@ async function getSomeNotePath(note) {
cur = parents[0];
}
path.push('root');
return path.reverse().join('/');
}
@@ -637,12 +637,6 @@ async function createNote(node, parentNoteId, target, extraOptions = {}) {
if (noteDetailService.getActiveTabNoteType() !== 'text') {
extraOptions.saveSelection = false;
}
else {
// just disable this feature altogether - there's a problem that note containing image or table at the beginning
// of the content will be auto-selected by CKEditor and then CTRL-P with no user interaction will automatically save
// the selection - see https://github.com/ckeditor/ckeditor5/issues/1384
extraOptions.saveSelection = false;
}
if (extraOptions.saveSelection) {
[extraOptions.title, extraOptions.content] = parseSelectedHtml(window.cutToNote.getSelectedHtml());
@@ -820,13 +814,13 @@ keyboardActionService.setGlobalActionHandler('CreateNoteAfter', async () => {
});
});
async function createNoteInto() {
async function createNoteInto(saveSelection = false) {
const node = getActiveNode();
if (node) {
await createNote(node, node.data.noteId, 'into', {
isProtected: node.data.isProtected,
saveSelection: true
saveSelection: saveSelection
});
}
}
@@ -875,7 +869,9 @@ async function reloadNotes(noteIds, activateNotePath = null) {
}
}
window.glob.createNoteInto = createNoteInto;
window.glob.cutIntoNote = () => createNoteInto(true);
keyboardActionService.setGlobalActionHandler('CutIntoNote', () => createNoteInto(true));
keyboardActionService.setGlobalActionHandler('CreateNoteInto', createNoteInto);

View File

@@ -213,7 +213,11 @@ function closeActiveDialog() {
}
function isHtmlEmpty(html) {
return $("<div>").html(html).text().trim().length === 0 && !html.toLowerCase().includes('<img');
html = html.toLowerCase();
return $("<div>").html(html).text().trim().length === 0
&& !html.includes('<img')
&& !html.includes('<section');
}
async function clearBrowserCache() {

View File

@@ -172,7 +172,7 @@ function connectWebSocket() {
async function sendPing() {
if (Date.now() - lastPingTs > 30000) {
console.log(utils.now(), "Lost connection to server");
console.log(utils.now(), "Lost websocket connection to the backend");
}
if (ws.readyState === ws.OPEN) {

View File

@@ -81,7 +81,7 @@ function SetupModel() {
password: password1,
theme: theme
}).then(() => {
window.location.replace("./");
window.location.replace("./setup");
});
}
else if (this.setupType() === 'sync-from-server') {
@@ -131,12 +131,15 @@ async function checkOutstandingSyncs() {
const { stats, initialized } = await $.get('api/sync/stats');
if (initialized) {
window.location.replace("./");
const remote = require('electron').remote;
remote.app.relaunch();
remote.app.exit(0);
}
else {
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
const totalOutstandingSyncs = stats.outstandingPushes + stats.outstandingPulls;
$("#outstanding-syncs").html(totalOutstandingSyncs);
$("#outstanding-syncs").html(totalOutstandingSyncs);
}
}
function showAlert(message) {

View File

@@ -45,7 +45,7 @@ class EditedNotesWidget extends StandardWidget {
$item.append($("<i>").text(editedNote.title + " (deleted)"));
}
else {
$item.append(editedNote.notePath ? await linkService.createNoteLinkWithPath(editedNote.notePath.join("/")) : editedNote.title);
$item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title);
}
$list.append($item);

View File

@@ -1,18 +1,18 @@
import StandardWidget from "./standard_widget.js";
const TPL = `
<table class="note-info-table">
<table class="note-info-table" style="table-layout: fixed; width: 100%;">
<tr>
<th>Note ID:</th>
<td colspan="3" class="note-info-note-id"></td>
<th nowrap>Note ID:</th>
<td nowrap colspan="3" class="note-info-note-id"></td>
</tr>
<tr>
<th>Created:</th>
<td colspan="3" class="note-info-date-created"></td>
<th nowrap>Created:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-created"></td>
</tr>
<tr>
<th>Modified:</th>
<td colspan="3" class="note-info-date-modified"></td>
<th nowrap>Modified:</th>
<td nowrap colspan="3" style="overflow: hidden; text-overflow: ellipsis;" class="note-info-date-modified"></td>
</tr>
<tr>
<th>Type:</th>
@@ -39,10 +39,19 @@ class NoteInfoWidget extends StandardWidget {
const note = this.ctx.note;
$noteId.text(note.noteId);
$dateCreated.text(note.dateCreated);
$dateModified.text(note.dateModified);
$dateCreated
.text(note.dateCreated)
.attr("title", note.dateCreated);
$dateModified
.text(note.dateModified)
.attr("title", note.dateCreated);
$type.text(note.type);
$mime.text(note.mime).attr("title", note.mime);
$mime
.text(note.mime)
.attr("title", note.mime);
}
eventReceived(name, data) {

View File

@@ -39,7 +39,7 @@ class SimilarNotesWidget extends StandardWidget {
}
const $item = $("<li>")
.append(await linkService.createNoteLinkWithPath(similarNote.notePath.join("/")));
.append(await linkService.createNoteLink(similarNote.notePath.join("/"), {showNotePath: true}));
$list.append($item);
}

View File

@@ -6,31 +6,43 @@ body {
margin: 0 auto; /* center */
height: 100vh;
display: grid;
grid-template-areas: "header header"
"left-pane tabs"
"left-pane tab-container";
grid-template-rows: auto
auto
1fr;
justify-content: center;
grid-gap: 0;
display: flex;
flex-direction: column;
}
#container.zen-mode {
grid-template-areas:
"tab-container" !important;
grid-template-rows: auto
auto
!important;
grid-template-columns: 1fr !important;
#topbar {
display: flex;
}
.hidden-by-zen-mode {
display: none !important;
}
.gutter {
background: linear-gradient(to bottom, transparent, var(--accented-background-color), transparent);
}
.gutter:hover {
background: linear-gradient(to bottom, transparent, var(--main-border-color), transparent);
}
.gutter.gutter-horizontal {
cursor: col-resize;
}
.gutter.gutter-vertical {
cursor: row-resize;
}
#center-pane {
display: flex;
flex-direction: column;
}
#note-tab-container {
grid-area: tab-container;
min-height: 0;
padding-left: 10px;
width: 100%;
}
#search-box {
@@ -49,20 +61,19 @@ body {
}
#left-pane {
grid-area: left-pane;
height: 100%;
display: flex;
flex-direction: column;
}
#header {
grid-area: header;
#toolbar {
background-color: var(--header-background-color);
display: flex;
align-items: center;
padding-top: 4px;
}
#header button {
#toolbar button {
padding: 1px 5px 1px 5px;
font-size: smaller;
margin-bottom: 2px;
@@ -71,12 +82,12 @@ body {
border-color: transparent !important;
}
#header button.btn-sm .bx {
#toolbar button.btn-sm .bx {
position: relative;
top: 1px;
}
#header button:hover {
#toolbar button:hover {
border-color: var(--button-border-color) !important;
}
@@ -90,7 +101,7 @@ body {
padding: 3px 0 3px 0;
border: 1px solid var(--main-border-color);
border-radius: 7px;
margin: 5px 15px 5px 5px;
margin: 3px 5px 5px 5px;
}
.dropdown-menu {
@@ -191,7 +202,7 @@ body {
.note-title-row {
flex-grow: 0;
flex-shrink: 0;
margin-top: 10px;
padding-top: 2px;
}
.note-title {
@@ -206,12 +217,13 @@ body {
.note-tab-row {
box-sizing: border-box;
position: relative;
height: 33px;
height: 34px;
min-height: 34px;
width: 100%;
background: var(--main-background-color);
border-radius: 5px 5px 0 0;
overflow: hidden;
grid-area: tabs;
margin-top: 5px;
margin-top: 2px;
}
.note-tab-row * {
box-sizing: inherit;
@@ -244,7 +256,7 @@ body {
text-align: center;
font-size: 24px;
cursor: pointer;
border-bottom: 1px solid var(--button-border-color);
border-bottom: 1px solid var(--main-border-color);
}
.note-new-tab:hover {
@@ -253,9 +265,15 @@ body {
}
.tab-row-filler {
-webkit-app-region: drag;
position: absolute;
left: 0;
background: linear-gradient(to right, var(--button-border-color), transparent);
height: 33px;
}
.tab-row-filler .tab-row-border {
position: relative;
background: linear-gradient(to right, var(--main-border-color), transparent);
height: 1px;
margin-top: 32px;
}
@@ -288,12 +306,12 @@ body {
overflow: hidden;
pointer-events: all;
background-color: var(--accented-background-color);
border-bottom: 1px solid var(--button-border-color);
border-bottom: 1px solid var(--main-border-color);
}
.note-tab-row .note-tab[active] .note-tab-wrapper {
background-color: var(--main-background-color);
border: 1px solid var(--button-border-color);
border: 1px solid var(--main-border-color);
border-bottom: 0;
font-weight: bold;
}
@@ -385,30 +403,26 @@ body {
transition: transform 120ms ease-in-out;
}
.hide-sidebar-button {
color: var(--main-text-color);
background: none;
border: 1px solid transparent;
padding: 2px 8px 2px 8px;
border-radius: 2px;
#hide-sidebar-button, #show-sidebar-button {
position: fixed;
bottom: 10px;
right: 10px;
z-index: 1000;
}
.hide-sidebar-button:hover {
border-color: var(--button-border-color);
}
.note-detail-sidebar {
#right-pane {
overflow: auto;
padding-top: 12px;
padding-top: 4px;
padding-left: 7px;
font-size: 90%;
height: 100%;
}
.note-detail-sidebar .card {
#right-pane .card {
border: 0;
}
.note-detail-sidebar .card-header {
#right-pane .card-header {
background: inherit;
padding: 5px 10px 5px 10px;
width: 99%; /* to give minimal right margin */
@@ -421,7 +435,7 @@ body {
justify-content: space-between;
}
.note-detail-sidebar .widget-title {
#right-pane .widget-title {
border-radius: 0;
padding: 0;
border: 0;
@@ -431,29 +445,29 @@ body {
color: var(--muted-text-color) !important;
}
.note-detail-sidebar .widget-header-action {
#right-pane .widget-header-action {
color: var(--link-color) !important;
cursor: pointer;
}
.note-detail-sidebar .widget-help {
#right-pane .widget-help {
color: var(--muted-text-color);
position: relative;
top: 2px;
}
.note-detail-sidebar .widget-help.no-link:hover {
#right-pane .widget-help.no-link:hover {
cursor: default;
text-decoration: none;
}
.note-detail-sidebar .card-body {
#right-pane .card-body {
width: 100%;
padding: 8px;
border: 0;
}
.note-detail-sidebar .card-body ul {
#right-pane .card-body ul {
padding-left: 25px;
margin-bottom: 5px;
}
@@ -485,4 +499,8 @@ body {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#title-bar-buttons {
min-width: 100px;
}

View File

@@ -105,6 +105,7 @@ span.fancytree-node.muted { opacity: 0.6; }
flex-direction: column;
flex-grow: 100;
height: 100%;
width: 100%;
}
.note-detail-component-wrapper {
@@ -159,6 +160,10 @@ span.fancytree-node.muted { opacity: 0.6; }
background-position: center;
}
.zen-mode #center-pane {
width: 100% !important;
}
ul.fancytree-container {
outline: none !important;
background-color: inherit !important;
@@ -333,7 +338,7 @@ div.ui-tooltip {
color: #888 !important;
}
.dropdown-menu a:hover:not(.disabled), li.dropdown-item:hover:not(.disabled) {
.dropdown-menu a:hover:not(.disabled), li.dropdown-item:hover:not(.disabled), div.dropdown-item:hover:not(.disabled) {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
cursor: pointer;
@@ -474,6 +479,17 @@ button.icon-button {
width: 15em;
}
#global-menu-wrapper {
height: 35px;
border-bottom: 1px solid var(--main-border-color);
}
#global-menu button {
margin-right: 10px;
height: 33px;
border-bottom: none;
}
#global-menu .dropdown-menu {
width: 20em;
}
@@ -578,7 +594,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
padding: 10px;
}
.note-detail-render-help {
.note-detail-render-help, .note-detail-book-help {
margin: 50px;
padding: 20px;
}
@@ -935,4 +951,30 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
font-weight: bold !important;
color: inherit !important;
vertical-align: baseline !important;
}
.ck-content .todo-list .todo-list__label > input:before {
border: 1px solid var(--muted-text-color) !important;
}
.ck-link-actions .ck-tooltip {
/* force hide the tooltip since it shows misleading "open link in new tab */
display: none !important;
}
.include-note {
margin: 20px;
padding: 20px;
border-radius: 10px;
background-color: var(--accented-background-color);
}
.include-note.ck-placeholder::before { /* remove placeholder in otherwise empty note */
content: '' !important;
}
.alert-warning {
color: var(--main-text-color) !important;
background-color: var(--accented-background-color) !important;
border-color: var(--main-border-color) !important;
}

View File

@@ -13,17 +13,11 @@ const ALLOWED_OPTIONS = new Set([
'syncServerHost',
'syncServerTimeout',
'syncProxy',
'leftPaneMinWidth',
'leftPaneWidthPercent',
'sidebarMinWidth',
'sidebarWidthPercent',
'showSidebarInNewTab',
'hoistedNoteId',
'mainFontSize',
'treeFontSize',
'detailFontSize',
'openTabs',
'hideTabRowForOneTab',
'noteInfoWidget',
'attributesWidget',
'linkMapWidget',
@@ -36,7 +30,11 @@ const ALLOWED_OPTIONS = new Set([
'spellCheckEnabled',
'spellCheckLanguageCode',
'imageMaxWidthHeight',
'imageJpegQuality'
'imageJpegQuality',
'leftPaneWidth',
'rightPaneWidth',
'rightPaneVisible',
'nativeTitleBarVisible'
]);
async function getOptions() {

View File

@@ -10,6 +10,8 @@ const contentHashService = require('../../services/content_hash');
const log = require('../../services/log');
const syncOptions = require('../../services/sync_options');
const dateUtils = require('../../services/date_utils');
const entityConstructor = require('../../entities/entity_constructor');
const utils = require('../../services/utils');
async function testSync() {
try {
@@ -47,7 +49,7 @@ async function getStats() {
async function checkSync() {
return {
hashes: await contentHashService.getHashes(),
entityHashes: await contentHashService.getEntityHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync')
};
}
@@ -137,6 +139,15 @@ async function syncFinished() {
await sqlInit.dbInitialized();
}
async function queueSector(req) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector);
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
}
module.exports = {
testSync,
checkSync,
@@ -147,5 +158,6 @@ module.exports = {
getChanged,
update,
getStats,
syncFinished
syncFinished,
queueSector
};

View File

@@ -6,6 +6,7 @@ const attributeService = require('../services/attributes');
const config = require('../services/config');
const optionService = require('../services/options');
const log = require('../services/log');
const env = require('../services/env');
async function index(req, res) {
const options = await optionService.getOptionsMap();
@@ -18,19 +19,14 @@ async function index(req, res) {
res.render(view, {
csrfToken: csrfToken,
theme: options.theme,
leftPaneMinWidth: parseInt(options.leftPaneMinWidth),
leftPaneWidthPercent: parseInt(options.leftPaneWidthPercent),
rightPaneWidthPercent: 100 - parseInt(options.leftPaneWidthPercent),
sidebarMinWidth: parseInt(options.sidebarMinWidth),
sidebarWidthPercent: parseInt(options.sidebarWidthPercent),
contentWidthPercent: 100 - parseInt(options.sidebarWidthPercent),
mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize),
sourceId: await sourceIdService.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: await getAppCssNoteIds()
appCssNoteIds: await getAppCssNoteIds(),
isDev: env.isDev()
});
}

View File

@@ -114,7 +114,7 @@ function register(app) {
route(GET, '/login', [auth.checkAppInitialized], loginRoute.loginPage);
route(POST, '/login', [], loginRoute.login);
route(POST, '/logout', [csrfMiddleware, auth.checkAuth], loginRoute.logout);
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
route(GET, '/setup', [], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
@@ -199,6 +199,7 @@ function register(app) {
route(GET, '/api/sync/changed', [auth.checkApiAuth], syncApiRoute.getChanged, apiResultHandler);
route(PUT, '/api/sync/update', [auth.checkApiAuth], syncApiRoute.update, apiResultHandler);
route(POST, '/api/sync/finished', [auth.checkApiAuth], syncApiRoute.syncFinished, apiResultHandler);
route(POST, '/api/sync/queue-sector/:entityName/:sector', [auth.checkApiAuth], syncApiRoute.queueSector, apiResultHandler);
route(GET, '/api/sync/stats', [], syncApiRoute.getStats, apiResultHandler);
apiRoute(POST, '/api/recent-notes', recentNotesRoute.addRecentNote);

View File

@@ -2,10 +2,18 @@
const sqlInit = require('../services/sql_init');
const setupService = require('../services/setup');
const utils = require('../services/utils');
const windowService = require('../services/window');
async function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) {
res.redirect('/');
if (utils.isElectron()) {
await windowService.createMainWindow();
windowService.closeSetupWindow();
}
else {
res.redirect('/');
}
}
// we got here because DB is not completely initialized so if schema exists

View File

@@ -5,7 +5,7 @@ const packageJson = require('../../package');
const {TRILIUM_DATA_DIR} = require('./data_dir');
const APP_DB_VERSION = 155;
const SYNC_VERSION = 12;
const SYNC_VERSION = 13;
const CLIPPER_PROTOCOL_VERSION = "1.0";
module.exports = {

View File

@@ -83,7 +83,7 @@ async function checkBasicAuth(req, res, next) {
const dbUsername = await optionService.getOption('username');
if (dbUsername !== username || !await passwordEncryptionService.verifyPassword(password)) {
res.status(401).send("Not authorized");
res.status(401).send('Incorrect username and/or password');
}
else {
next();

View File

@@ -204,7 +204,7 @@ function BackendScriptApi(currentNote, apiParams) {
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({
parentNoteId,
title,
content: JSON.stringify(content),
content: JSON.stringify(content, null, '\t'),
type: 'code',
mime: 'application/json'
});

View File

@@ -1 +1 @@
module.exports = { buildDate:"2019-12-18T20:21:06+01:00", buildRevision: "5988776b7ea358a117556cf9aaf0e3c78cb705b4" };
module.exports = { buildDate:"2020-01-04T22:01:20+01:00", buildRevision: "3b8b4da149fbc1b17d09253693823f5135a55f2e" };

View File

@@ -21,17 +21,20 @@ module.exports = function(filters, selectedColumns = 'notes.*') {
"notes": null
};
let attrFilterId = 1;
function getAccessor(property) {
let accessor;
if (!VIRTUAL_ATTRIBUTES.includes(property)) {
const alias = "attr_" + property;
// not reusing existing filters to support multi-valued filters e.g. "@tag=christmas @tag=shopping"
// can match notes because @tag can be both "shopping" and "christmas"
const alias = "attr_" + property + "_" + attrFilterId++;
if (!(alias in joins)) {
joins[alias] = `LEFT JOIN attributes AS ${alias} `
+ `ON ${alias}.noteId = notes.noteId `
+ `AND ${alias}.name = '${property}' AND ${alias}.isDeleted = 0`;
}
// forcing to use particular index since SQLite query planner would often choose something pretty bad
joins[alias] = `LEFT JOIN attributes AS ${alias} INDEXED BY IDX_attributes_noteId_index `
+ `ON ${alias}.noteId = notes.noteId `
+ `AND ${alias}.name = '${property}' AND ${alias}.isDeleted = 0`;
accessor = `${alias}.value`;
}

View File

@@ -3,7 +3,6 @@
const sql = require('./sql');
const utils = require('./utils');
const log = require('./log');
const ws = require('./ws.js');
const ApiToken = require('../entities/api_token');
const Branch = require('../entities/branch');
const Note = require('../entities/note');
@@ -12,65 +11,73 @@ const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
async function getHash(tableName, primaryKeyName, whereBranch) {
// subselect is necessary to have correct ordering in GROUP_CONCAT
const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${primaryKeyName})`;
async function getSectorHashes(tableName, primaryKeyName, whereBranch) {
const hashes = await sql.getRows(`SELECT ${primaryKeyName} AS id, hash FROM ${tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '')
+ ` ORDER BY ${primaryKeyName}`);
let contentToHash = await sql.getValue(query);
const map = {};
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";
for (const {id, hash} of hashes) {
map[id[0]] = (map[id[0]] || "") + hash;
}
return utils.hash(contentToHash);
for (const key in map) {
map[key] = utils.hash(map[key]);
}
return map;
}
async function getHashes() {
async function getEntityHashes() {
const startTime = new Date();
const hashes = {
notes: await getHash(Note.entityName, Note.primaryKeyName),
note_contents: await getHash("note_contents", "noteId"),
branches: await getHash(Branch.entityName, Branch.primaryKeyName),
note_revisions: await getHash(NoteRevision.entityName, NoteRevision.primaryKeyName),
note_revision_contents: await getHash("note_revision_contents", "noteRevisionId"),
recent_notes: await getHash(RecentNote.entityName, RecentNote.primaryKeyName),
options: await getHash(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
attributes: await getHash(Attribute.entityName, Attribute.primaryKeyName),
api_tokens: await getHash(ApiToken.entityName, ApiToken.primaryKeyName),
notes: await getSectorHashes(Note.entityName, Note.primaryKeyName),
note_contents: await getSectorHashes("note_contents", "noteId"),
branches: await getSectorHashes(Branch.entityName, Branch.primaryKeyName),
note_revisions: await getSectorHashes(NoteRevision.entityName, NoteRevision.primaryKeyName),
note_revision_contents: await getSectorHashes("note_revision_contents", "noteRevisionId"),
recent_notes: await getSectorHashes(RecentNote.entityName, RecentNote.primaryKeyName),
options: await getSectorHashes(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
attributes: await getSectorHashes(Attribute.entityName, Attribute.primaryKeyName),
api_tokens: await getSectorHashes(ApiToken.entityName, ApiToken.primaryKeyName),
};
const elapseTimeMs = Date.now() - startTime.getTime();
const elapsedTimeMs = Date.now() - startTime.getTime();
log.info(`Content hash computation took ${elapseTimeMs}ms`);
log.info(`Content hash computation took ${elapsedTimeMs}ms`);
return hashes;
}
async function checkContentHashes(otherHashes) {
const hashes = await getHashes();
let allChecksPassed = true;
const entityHashes = await getEntityHashes();
const failedChecks = [];
for (const key in hashes) {
if (hashes[key] !== otherHashes[key]) {
allChecksPassed = false;
for (const entityName in entityHashes) {
const thisSectorHashes = entityHashes[entityName];
const otherSectorHashes = otherHashes[entityName];
log.info(`Content hash check for ${key} FAILED. Local is ${hashes[key]}, remote is ${otherHashes[key]}`);
const sectors = new Set(Object.keys(thisSectorHashes).concat(Object.keys(otherSectorHashes)));
if (key !== 'recent_notes') {
// let's not get alarmed about recent notes which get updated often and can cause failures in race conditions
ws.sendMessageToAllClients({type: 'sync-hash-check-failed'});
for (const sector of sectors) {
if (thisSectorHashes[sector] !== otherSectorHashes[sector]) {
log.info(`Content hash check for ${entityName} sector ${sector} FAILED. Local is ${thisSectorHashes[sector]}, remote is ${otherSectorHashes[sector]}`);
failedChecks.push({ entityName, sector });
}
}
}
if (allChecksPassed) {
if (failedChecks.length === 0) {
log.info("Content hash checks PASSED");
}
return failedChecks;
}
module.exports = {
getHashes,
getEntityHashes,
checkContentHashes
};

View File

@@ -2,6 +2,7 @@
const html = require('html');
const repository = require('../repository');
const dateUtils = require('../date_utils');
const tar = require('tar-stream');
const path = require('path');
const mimeTypes = require('mime-types');
@@ -10,6 +11,8 @@ const packageInfo = require('../../../package.json');
const utils = require('../utils');
const protectedSessionService = require('../protected_session');
const sanitize = require("sanitize-filename");
const fs = require("fs");
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
/**
* @param {TaskContext} taskContext
@@ -187,13 +190,13 @@ async function exportToTar(taskContext, branch, format, res) {
for (let i = 0; i < targetPath.length - 1; i++) {
const meta = noteIdToMeta[targetPath[i]];
url += meta.dirFileName + '/';
url += encodeURIComponent(meta.dirFileName) + '/';
}
const meta = noteIdToMeta[targetPath[targetPath.length - 1]];
// link can target note which is only "folder-note" and as such will not have a file in an export
url += meta.dataFileName || meta.dirFileName;
url += encodeURIComponent(meta.dataFileName || meta.dirFileName);
return url;
}
@@ -202,13 +205,13 @@ async function exportToTar(taskContext, branch, format, res) {
content = content.replace(/src="[^"]*api\/images\/([a-zA-Z0-9]+)\/[^"]*"/g, (match, targetNoteId) => {
const url = getTargetUrl(targetNoteId, noteMeta);
return url ? `src="${encodeURIComponent(url)}"` : match;
return url ? `src="${url}"` : match;
});
content = content.replace(/href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g, (match, targetNoteId) => {
const url = getTargetUrl(targetNoteId, noteMeta);
return url ? `href="${encodeURIComponent(url)}"` : match;
return url ? `href="${url}"` : match;
});
return content;
@@ -223,8 +226,13 @@ async function exportToTar(taskContext, branch, format, res) {
if (noteMeta.format === 'html') {
if (!content.substr(0, 100).toLowerCase().includes("<html")) {
const cssUrl = "../".repeat(noteMeta.notePath.length - 1) + 'style.css';
content = `<html>
<head><meta charset="utf-8"></head>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="${cssUrl}">
</head>
<body>
<h1>${utils.escapeHtml(title)}</h1>
${content}
@@ -255,7 +263,7 @@ ${content}
if (noteMeta.isClone) {
const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta);
let content = `<p>This is a clone of a note. Go to its <a href="${encodeURIComponent(targetUrl)}">primary location</a>.</p>`;
let content = `<p>This is a clone of a note. Go to its <a href="${targetUrl}">primary location</a>.</p>`;
content = prepareContent(noteMeta.title, content, noteMeta);
@@ -270,7 +278,11 @@ ${content}
if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, await note.getContent(), noteMeta);
pack.entry({name: filePathPrefix + noteMeta.dataFileName, size: content.length}, content);
pack.entry({
name: filePathPrefix + noteMeta.dataFileName,
size: content.length,
mtime: dateUtils.parseDateTime(note.utcDateModified)
}, content);
}
taskContext.increaseProgressCount();
@@ -278,7 +290,11 @@ ${content}
if (noteMeta.children && noteMeta.children.length > 0) {
const directoryPath = filePathPrefix + noteMeta.dirFileName;
pack.entry({name: directoryPath, type: 'directory'});
pack.entry({
name: directoryPath,
type: 'directory',
mtime: dateUtils.parseDateTime(note.utcDateModified)
});
for (const childMeta of noteMeta.children) {
await saveNote(childMeta, directoryPath + '/');
@@ -314,7 +330,15 @@ ${content}
return html + '</li>';
}
const fullHtml = '<html><head><meta charset="utf-8"></head><body><ul>' + saveNavigationInner(rootMeta) + '</ul></body></html>'
const fullHtml = `<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<ul>${saveNavigationInner(rootMeta)}</ul>
</body>
</html>`;
const prettyHtml = html.prettyPrint(fullHtml, {indent_size: 2});
pack.entry({name: navigationMeta.dataFileName, size: prettyHtml.length}, prettyHtml);
@@ -351,6 +375,12 @@ ${content}
pack.entry({name: indexMeta.dataFileName, size: fullHtml.length}, fullHtml);
}
function saveCss(rootMeta, cssMeta) {
const cssContent = fs.readFileSync(RESOURCE_DIR + '/libraries/ckeditor/ckeditor-content.css');
pack.entry({name: cssMeta.dataFileName, size: cssContent.length}, cssContent);
}
const existingFileNames = format === 'html' ? ['navigation', 'index'] : [];
const rootMeta = await getNoteMeta(branch, { notePath: [] }, existingFileNames);
@@ -360,7 +390,7 @@ ${content}
files: [ rootMeta ]
};
let navigationMeta, indexMeta;
let navigationMeta, indexMeta, cssMeta;
if (format === 'html') {
navigationMeta = {
@@ -376,6 +406,13 @@ ${content}
};
metaFile.files.push(indexMeta);
cssMeta = {
noImport: true,
dataFileName: "style.css"
};
metaFile.files.push(cssMeta);
}
for (const noteMeta of Object.values(noteIdToMeta)) {
@@ -397,6 +434,7 @@ ${content}
if (format === 'html') {
saveNavigation(rootMeta, navigationMeta);
saveIndex(rootMeta, indexMeta);
saveCss(rootMeta, cssMeta);
}
pack.finalize();

View File

@@ -75,14 +75,17 @@ async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSw
}
async function shrinkImage(buffer, originalName) {
const resizedImage = await resize(buffer);
// we do resizing with max (100) quality which will be trimmed during optimization step next
const resizedImage = await resize(buffer, 100);
let finalImageBuffer;
const jpegQuality = await optionService.getOptionInt('imageJpegQuality');
try {
finalImageBuffer = await optimize(resizedImage);
finalImageBuffer = await optimize(resizedImage, jpegQuality);
} catch (e) {
log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
finalImageBuffer = resizedImage;
finalImageBuffer = await resize(buffer, jpegQuality);
}
// if resizing & shrinking did not help with size then save the original
@@ -94,7 +97,7 @@ async function shrinkImage(buffer, originalName) {
return finalImageBuffer;
}
async function resize(buffer) {
async function resize(buffer, quality) {
const imageMaxWidthHeight = await optionService.getOptionInt('imageMaxWidthHeight');
const image = await jimp.read(buffer);
@@ -106,8 +109,7 @@ async function resize(buffer) {
image.resize(jimp.AUTO, imageMaxWidthHeight);
}
// we do resizing with max quality which will be trimmed during optimization step next
image.quality(100);
image.quality(quality);
// when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
image.background(0xFFFFFFFF);
@@ -115,11 +117,11 @@ async function resize(buffer) {
return image.getBufferAsync(jimp.MIME_JPEG);
}
async function optimize(buffer) {
async function optimize(buffer, jpegQuality) {
return await imagemin.buffer(buffer, {
plugins: [
imageminMozJpeg({
quality: await optionService.getOptionInt('imageJpegQuality')
quality: jpegQuality
}),
imageminPngQuant({
quality: [0, 0.7]

View File

@@ -274,6 +274,9 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return /^(?:[a-z]+:)?\/\//i.test(url);
}
content = content.replace(/<html.*<body[^>]*>/gis, "");
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/src="([^"]*)"/g, (match, url) => {
url = decodeURIComponent(url);
@@ -298,9 +301,6 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return `href="#root/${targetNoteId}"`;
});
content = content.replace(/<html.*<body[^>]*>/gis, "");
content = content.replace(/<\/body>.*<\/html>/gis, "");
content = content.replace(/<h1>([^<]*)<\/h1>/gi, (match, text) => {
if (noteTitle.trim() === text.trim()) {
return ""; // remove whole H1 tag

View File

@@ -255,6 +255,11 @@ const DEFAULT_KEYBOARD_ACTIONS = [
defaultShortcuts: [],
description: "Pastes Markdown from clipboard into text note"
},
{
actionName: "CutIntoNote",
defaultShortcuts: [],
description: "Cuts the selection from the current note and creates subnote with the selected text"
},
{
separator: "Other"

View File

@@ -48,7 +48,7 @@ function deriveMime(type, mime) {
mime = 'text/plain';
} else if (['relation-map', 'search'].includes(type)) {
mime = 'application/json';
} else if (type === 'render') {
} else if (['render', 'book'].includes(type)) {
mime = '';
}

View File

@@ -33,7 +33,6 @@ async function initNotSyncedOptions(initialized, startNotePath = 'root', opts =
notePath: startNotePath,
active: true,
sidebar: {
visible: true,
widgets: []
}
}
@@ -61,14 +60,9 @@ const defaultOptions = [
{ name: 'protectedSessionTimeout', value: '600', isSynced: true },
{ name: 'hoistedNoteId', value: 'root', isSynced: false },
{ name: 'zoomFactor', value: '1.0', isSynced: false },
{ name: 'leftPaneMinWidth', value: '350', isSynced: false },
{ name: 'leftPaneWidthPercent', value: '20', isSynced: false },
{ name: 'mainFontSize', value: '100', isSynced: false },
{ name: 'treeFontSize', value: '100', isSynced: false },
{ name: 'detailFontSize', value: '110', isSynced: false },
{ name: 'sidebarMinWidth', value: '350', isSynced: false },
{ name: 'sidebarWidthPercent', value: '25', isSynced: false },
{ name: 'showSidebarInNewTab', value: 'true', isSynced: false },
{ name: 'calendarWidget', value: '{"enabled":true,"expanded":true,"position":20}', isSynced: false },
{ name: 'editedNotesWidget', value: '{"enabled":true,"expanded":true,"position":50}', isSynced: false },
{ name: 'noteInfoWidget', value: '{"enabled":true,"expanded":true,"position":100}', isSynced: false },
@@ -79,11 +73,14 @@ const defaultOptions = [
{ name: 'similarNotesWidget', value: '{"enabled":true,"expanded":true,"position":600}', isSynced: false },
{ name: 'spellCheckEnabled', value: 'true', isSynced: false },
{ name: 'spellCheckLanguageCode', value: 'en-US', isSynced: false },
{ name: 'hideTabRowForOneTab', value: 'false', isSynced: false },
{ name: 'imageMaxWidthHeight', value: '1200', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true }
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneWidth', value: '25', isSynced: false },
{ name: 'rightPaneVisible', value: 'true', isSynced: false },
{ name: 'nativeTitleBarVisible', value: 'false', isSynced: false }
];
async function initStartupOptions() {

View File

@@ -1,6 +1,6 @@
const dayjs = require("dayjs");
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*([\p{L}\p{Number}_/-]+|"[^"]+"))?/igu;
const filterRegex = /(\b(AND|OR)\s+)?@(!?)([\p{L}\p{Number}_]+|"[^"]+")\s*((=|!=|<|<=|>|>=|!?\*=|!?=\*|!?\*=\*)\s*(\S+|"[^"]+"))?/igu;
const smartValueRegex = /^(NOW|TODAY|WEEK|MONTH|YEAR) *([+\-] *\d+)?$/i;
function calculateSmartValue(v) {

View File

@@ -71,7 +71,8 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
'user': username,
'pass': password
},
proxy: syncProxy
proxy: syncProxy,
timeout: 30000 // seed request should not take long
});
if (resp.syncVersion !== appInfo.syncVersion) {

View File

@@ -15,6 +15,8 @@ const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
const request = require('./request');
const ws = require('./ws');
const syncTableService = require('./sync_table');
const entityConstructor = require('../entities/entity_constructor');
let proxyToggle = true;
@@ -30,17 +32,22 @@ async function sync() {
return { success: false, message: 'Sync not configured' };
}
const syncContext = await login();
let continueSync = false;
await pushSync(syncContext);
do {
const syncContext = await login();
await pullSync(syncContext);
await pushSync(syncContext);
await pushSync(syncContext);
await pullSync(syncContext);
await syncFinished(syncContext);
await pushSync(syncContext);
await checkContentHash(syncContext);
await syncFinished(syncContext);
continueSync = await checkContentHash(syncContext);
}
while (continueSync);
return {
success: true
@@ -225,7 +232,7 @@ async function checkContentHash(syncContext) {
if (await getLastSyncedPull() < resp.maxSyncId) {
log.info("There are some outstanding pulls, skipping content check.");
return;
return true;
}
const notPushedSyncs = await sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE id > ?)", [await getLastSyncedPush()]);
@@ -233,10 +240,22 @@ async function checkContentHash(syncContext) {
if (notPushedSyncs) {
log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
return;
return true;
}
await contentHashService.checkContentHashes(resp.hashes);
const failedChecks = await contentHashService.checkContentHashes(resp.entityHashes);
for (const {entityName, sector} of failedChecks) {
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
log.info(`Added sector ${sector} of ${entityName} to sync queue.`);
}
return failedChecks.length > 0;
}
async function syncRequest(syncContext, method, requestPath, body) {

View File

@@ -23,6 +23,6 @@ module.exports = {
// and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== 'disabled';
},
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')),
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')) || 60000,
getSyncProxy: async () => await get('syncProxy')
};

View File

@@ -6,7 +6,7 @@ const cls = require('./cls');
let syncs = [];
async function addEntitySync(entityName, entityId, sourceId) {
async function insertEntitySync(entityName, entityId, sourceId) {
const sync = {
entityName: entityName,
entityId: entityId,
@@ -16,11 +16,25 @@ async function addEntitySync(entityName, entityId, sourceId) {
sync.id = await sql.replace("sync", sync);
return sync;
}
async function addEntitySync(entityName, entityId, sourceId) {
const sync = await insertEntitySync(entityName, entityId, sourceId);
syncs.push(sync);
setTimeout(() => require('./ws').sendPingToAllClients(), 50);
}
async function addEntitySyncsForSector(entityName, entityPrimaryKey, sector) {
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]);
for (const entityId of entityIds) {
await insertEntitySync(entityName, entityId, 'content-check');
}
}
function getMaxSyncId() {
return syncs.length === 0 ? 0 : syncs[syncs.length - 1].id;
}
@@ -29,19 +43,19 @@ function getEntitySyncsNewerThan(syncId) {
return syncs.filter(s => s.id > syncId);
}
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
async function cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey) {
await sql.execute(`
DELETE
FROM sync
WHERE sync.entityName = '${entityName}'
AND sync.entityId NOT IN (SELECT ${entityKey} FROM ${entityName})`);
AND sync.entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
}
async function fillSyncRows(entityName, entityKey, condition = '') {
async function fillSyncRows(entityName, entityPrimaryKey, condition = '') {
try {
await cleanupSyncRowsForMissingEntities(entityName, entityKey);
await cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey);
const entityIds = await sql.getColumn(`SELECT ${entityKey} FROM ${entityName}`
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName}`
+ (condition ? ` WHERE ${condition}` : ''));
let createdCount = 0;
@@ -69,7 +83,7 @@ async function fillSyncRows(entityName, entityKey, condition = '') {
catch (e) {
// this is to fix migration from 0.30 to 0.32, can be removed later
// see https://github.com/zadam/trilium/issues/557
log.error(`Filling sync rows failed for ${entityName} ${entityKey} with error "${e.message}", continuing`);
log.error(`Filling sync rows failed for ${entityName} ${entityPrimaryKey} with error "${e.message}", continuing`);
}
}
@@ -101,5 +115,6 @@ module.exports = {
addEntitySync,
fillAllSyncRows,
getEntitySyncsNewerThan,
getMaxSyncId
getMaxSyncId,
addEntitySyncsForSector
};

View File

@@ -50,135 +50,155 @@ async function updateEntity(sync, entity, sourceId) {
}
}
async function updateNote(entity, sourceId) {
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
function shouldWeUpdateEntity(localEntity, remoteEntity) {
if (!localEntity) {
return true;
}
if (!origNote || origNote.utcDateModified < entity.utcDateModified) {
const localDate = localEntity.utcDateModified || localEntity.utcDateCreated;
const remoteDate = remoteEntity.utcDateModified || remoteEntity.utcDateCreated;
if (localDate < remoteDate) {
return true;
}
// this can happen in case of sync error when hashes are different but dates are the same - we should still update
if (localEntity.hash !== remoteEntity.hash && localDate === remoteDate) {
return true;
}
return false;
}
async function updateNote(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [remoteEntity.noteId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace("notes", entity);
await sql.replace("notes", remoteEntity);
await syncTableService.addNoteSync(entity.noteId, sourceId);
await syncTableService.addNoteSync(remoteEntity.noteId, sourceId);
});
log.info("Update/sync note " + entity.noteId);
log.info("Update/sync note " + remoteEntity.noteId);
}
}
async function updateNoteContent(entity, sourceId) {
const origNoteContent = await sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [entity.noteId]);
async function updateNoteContent(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [remoteEntity.noteId]);
if (!origNoteContent || origNoteContent.utcDateModified < entity.utcDateModified) {
entity.content = entity.content === null ? null : Buffer.from(entity.content, 'base64');
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.transactional(async () => {
await sql.replace("note_contents", entity);
await sql.replace("note_contents", remoteEntity);
await syncTableService.addNoteContentSync(entity.noteId, sourceId);
await syncTableService.addNoteContentSync(remoteEntity.noteId, sourceId);
});
log.info("Update/sync note content for noteId=" + entity.noteId);
log.info("Update/sync note content for noteId=" + remoteEntity.noteId);
}
}
async function updateBranch(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [entity.branchId]);
async function updateBranch(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [remoteEntity.branchId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
// isExpanded is not synced unless it's a new branch instance
// otherwise in case of full new sync we'll get all branches (even root) collapsed.
if (orig) {
delete entity.isExpanded;
if (localEntity) {
delete remoteEntity.isExpanded;
}
await sql.replace('branches', entity);
await sql.replace('branches', remoteEntity);
await syncTableService.addBranchSync(entity.branchId, sourceId);
await syncTableService.addBranchSync(remoteEntity.branchId, sourceId);
log.info("Update/sync branch " + entity.branchId);
log.info("Update/sync branch " + remoteEntity.branchId);
}
});
}
async function updateNoteRevision(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [entity.noteRevisionId]);
async function updateNoteRevision(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
await sql.replace('note_revisions', entity);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.replace('note_revisions', remoteEntity);
await syncTableService.addNoteRevisionSync(entity.noteRevisionId, sourceId);
await syncTableService.addNoteRevisionSync(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision " + entity.noteRevisionId);
log.info("Update/sync note revision " + remoteEntity.noteRevisionId);
}
});
}
async function updateNoteRevisionContent(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [entity.noteRevisionId]);
async function updateNoteRevisionContent(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
entity.content = entity.content === null ? null : Buffer.from(entity.content, 'base64');
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.replace('note_revision_contents', entity);
await sql.replace('note_revision_contents', remoteEntity);
await syncTableService.addNoteRevisionContentSync(entity.noteRevisionId, sourceId);
await syncTableService.addNoteRevisionContentSync(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision content " + entity.noteRevisionId);
log.info("Update/sync note revision content " + remoteEntity.noteRevisionId);
}
});
}
async function updateNoteReordering(entityId, entity, sourceId) {
async function updateNoteReordering(entityId, remote, sourceId) {
await sql.transactional(async () => {
for (const key in entity) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [entity[key], key]);
for (const key in remote) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remote[key], key]);
}
await syncTableService.addNoteReorderingSync(entityId, sourceId);
});
}
async function updateOptions(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [entity.name]);
async function updateOptions(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [remoteEntity.name]);
if (orig && !orig.isSynced) {
if (localEntity && !localEntity.isSynced) {
return;
}
await sql.transactional(async () => {
if (orig === null || orig.utcDateModified < entity.utcDateModified) {
await sql.replace('options', entity);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.replace('options', remoteEntity);
await syncTableService.addOptionsSync(entity.name, sourceId);
await syncTableService.addOptionsSync(remoteEntity.name, sourceId);
}
});
}
async function updateRecentNotes(entity, sourceId) {
const orig = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [entity.noteId]);
async function updateRecentNotes(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [remoteEntity.noteId]);
if (orig === null || orig.utcDateCreated < entity.utcDateCreated) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace('recent_notes', entity);
await sql.replace('recent_notes', remoteEntity);
await syncTableService.addRecentNoteSync(entity.noteId, sourceId);
await syncTableService.addRecentNoteSync(remoteEntity.noteId, sourceId);
});
}
}
async function updateAttribute(entity, sourceId) {
const origAttribute = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [entity.attributeId]);
async function updateAttribute(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [remoteEntity.attributeId]);
if (!origAttribute || origAttribute.utcDateModified <= entity.utcDateModified) {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => {
await sql.replace("attributes", entity);
await sql.replace("attributes", remoteEntity);
await syncTableService.addAttributeSync(entity.attributeId, sourceId);
await syncTableService.addAttributeSync(remoteEntity.attributeId, sourceId);
});
log.info("Update/sync attribute " + entity.attributeId);
log.info("Update/sync attribute " + remoteEntity.attributeId);
}
}

View File

@@ -12,8 +12,14 @@ class TaskContext {
this.data = data;
// progressCount is meant to represent just some progress - to indicate the task is not stuck
this.progressCount = 0;
this.lastSentCountTs = Date.now();
this.progressCount = -1; // we're incrementing immediatelly
this.lastSentCountTs = 0; // 0 will guarantee first message will be sent
// just the fact this has been initialized is a progress which should be sent to clients
// this is esp. important when importing big files/images which take long time to upload/process
// which means that first "real" increaseProgressCount() will be called quite late and user is without
// feedback until then
this.increaseProgressCount();
}
/** @return {TaskContext} */

View File

@@ -63,7 +63,8 @@ async function getNotes(noteIds) {
SELECT
noteId,
title,
isProtected,
contentLength,
isProtected,
type,
mime,
isDeleted

View File

@@ -53,6 +53,10 @@ function sanitizeSql(str) {
return str.replace(/'/g, "''");
}
function sanitizeSqlIdentifier(str) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
function prepareSqlForLike(prefix, str, suffix) {
const value = str
.replace(/\\/g, "\\\\")
@@ -174,6 +178,7 @@ module.exports = {
hash,
isEmptyOrWhitespace,
sanitizeSql,
sanitizeSqlIdentifier,
prepareSqlForLike,
stopWatch,
escapeHtml,

131
src/services/window.js Normal file
View File

@@ -0,0 +1,131 @@
const path = require('path');
const url = require("url");
const port = require('./port');
const optionService = require('./options');
const env = require('./env');
const log = require('./log');
const sqlInit = require('./sql_init');
const cls = require('./cls');
const keyboardActionsService = require('./keyboard_actions');
// Prevent window being garbage collected
/** @type {Electron.BrowserWindow} */
let mainWindow;
/** @type {Electron.BrowserWindow} */
let setupWindow;
async function createMainWindow() {
const windowStateKeeper = require('electron-window-state'); // should not be statically imported
const mainWindowState = windowStateKeeper({
// default window width & height so it's usable on 1600 * 900 display (including some extra panels etc.)
defaultWidth: 1200,
defaultHeight: 800
});
const {BrowserWindow} = require('electron'); // should not be statically imported
mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
title: 'Trilium Notes',
webPreferences: {
nodeIntegration: true
},
frame: await optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
});
mainWindowState.manage(mainWindow);
mainWindow.setMenuBarVisibility(false);
mainWindow.loadURL('http://127.0.0.1:' + await port);
mainWindow.on('closed', () => mainWindow = null);
mainWindow.webContents.on('new-window', (e, url) => {
if (url !== mainWindow.webContents.getURL()) {
e.preventDefault();
require('electron').shell.openExternal(url);
}
});
// prevent drag & drop to navigate away from trilium
mainWindow.webContents.on('will-navigate', (ev, targetUrl) => {
const parsedUrl = url.parse(targetUrl);
// we still need to allow internal redirects from setup and migration pages
if (!['localhost', '127.0.0.1'].includes(parsedUrl.hostname) || (parsedUrl.path && parsedUrl.path !== '/')) {
ev.preventDefault();
}
});
}
function getIcon() {
return path.join(__dirname, '../../images/app-icons/png/256x256' + (env.isDev() ? '-dev' : '') + '.png');
}
async function createSetupWindow() {
const {BrowserWindow} = require('electron'); // should not be statically imported
setupWindow = new BrowserWindow({
width: 800,
height: 800,
title: 'Trilium Notes Setup',
icon: getIcon(),
webPreferences: {
// necessary for e.g. utils.isElectron()
nodeIntegration: true
}
});
setupWindow.setMenuBarVisibility(false);
setupWindow.loadURL('http://127.0.0.1:' + await port);
setupWindow.on('closed', () => setupWindow = null);
}
function closeSetupWindow() {
if (setupWindow) {
setupWindow.close();
}
}
async function registerGlobalShortcuts() {
const {globalShortcut} = require('electron');
await sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions();
for (const action of allActions) {
if (!action.effectiveShortcuts) {
continue;
}
for (const shortcut of action.effectiveShortcuts) {
if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => {
// window may be hidden / not in focus
mainWindow.focus();
mainWindow.webContents.send('globalShortcut', action.actionName);
}));
if (result) {
log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`);
}
else {
log.info(`Could not register global shortcut ${translatedShortcut}`);
}
}
}
}
}
module.exports = {
createMainWindow,
createSetupWindow,
closeSetupWindow,
registerGlobalShortcuts
};

39
src/views/center.ejs Normal file
View File

@@ -0,0 +1,39 @@
<div id="center-pane">
<div id="note-tab-container">
<div class="note-tab-content note-tab-content-template">
<div class="note-detail-content">
<% include title.ejs %>
<div class="note-detail-script-area"></div>
<table class="note-detail-promoted-attributes"></table>
<div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component">
<div class="note-detail-text-editor" tabindex="10000"></div>
</div>
<div class="note-detail-code note-detail-component">
<div class="note-detail-code-editor"></div>
</div>
<% include details/empty.ejs %>
<% include details/search.ejs %>
<% include details/render.ejs %>
<% include details/file.ejs %>
<% include details/image.ejs %>
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div>
</div>
</div>
</div>
</div>

View File

@@ -10,48 +10,9 @@
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
<div id="container" style="display: none; grid-template-columns: minmax(<%= leftPaneMinWidth %>px, <%= leftPaneWidthPercent %>fr) minmax(0, <%= rightPaneWidthPercent %>fr)">
<div id="header" class="hide-in-zen-mode">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action bx bx-left-arrow-circle"></a>
<a id="history-forward-button" title="Go to next note." class="icon-action bx bx-right-arrow-circle"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" data-kb-action="JumpToNote">
<span class="bx bx-crosshair"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button" data-kb-action="ShowRecentChanges">
<span class="bx bx-history"></span>
Recent changes
</button>
<button class="btn btn-sm"
id="enter-protected-session-button"
title="Enter protected session to be able to find and view protected notes">
<span class="bx bx-log-in"></span>
Enter protected session
</button>
<button class="btn btn-sm"
id="leave-protected-session-button"
title="Leave protected session so that protected notes are not accessible any more."
style="display: none;">
<span class="bx bx-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
<div>
<div id="container" style="display: none;">
<div id="topbar">
<div id="global-menu-wrapper">
<div class="dropdown" id="global-menu">
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle">
<span class="bx bx-menu"></span>
@@ -123,54 +84,109 @@
</div>
</div>
</div>
</div>
<div id="left-pane" class="hide-in-zen-mode">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
<a id="collapse-tree-button" title="Collapse note tree" data-kb-action="CollapseTree" class="icon-action bx bx-layer-minus"></a>
<a id="scroll-to-active-note-button" title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="icon-action bx bx-crosshair"></a>
<a id="toggle-search-button" title="Search in notes" data-kb-action="SearchNotes" class="icon-action bx bx-search"></a>
<div class="note-tab-row">
<div class="note-tab-row-content"></div>
</div>
<div id="search-box">
<div class="form-group">
<div class="input-group">
<input name="search-text" id="search-text" class="form-control"
placeholder="Search text, labels" autocomplete="off">
<div id="title-bar-buttons" style="display: none;">
<button class="btn icon-action bx bx-minus" id="minimize-btn"></button>
<button class="btn icon-action bx bx-checkbox" id="maximize-btn"></button>
<button class="btn icon-action bx bx-x" id="close-btn"></button>
</div>
</div>
<div class="input-group-append">
<button id="do-search-button" class="btn btn-sm icon-button bx bx-search" title="Search (enter)"></button>
<div id="toolbar" class="hide-in-zen-mode">
<div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action bx bx-left-arrow-circle"></a>
<a id="history-forward-button" title="Go to next note." class="icon-action bx bx-right-arrow-circle"></a>
</div>
<div style="flex-grow: 100; display: flex;">
<button class="btn btn-sm" id="jump-to-note-dialog-button" data-kb-action="JumpToNote">
<span class="bx bx-crosshair"></span>
Jump to note
</button>
<button class="btn btn-sm" id="recent-changes-button" data-kb-action="ShowRecentChanges">
<span class="bx bx-history"></span>
Recent changes
</button>
<button class="btn btn-sm"
id="enter-protected-session-button"
title="Enter protected session to be able to find and view protected notes">
<span class="bx bx-log-in"></span>
Enter protected session
</button>
<button class="btn btn-sm"
id="leave-protected-session-button"
title="Leave protected session so that protected notes are not accessible any more."
style="display: none;">
<span class="bx bx-log-out"></span>
Leave protected session
</button>
</div>
<div id="plugin-buttons">
</div>
</div>
<div style="display: flex; flex-grow: 1; flex-shrink: 1; min-height: 0;">
<div id="left-pane" class="hide-in-zen-mode">
<div id="global-buttons">
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action bx bx-folder-plus"></a>
<a id="collapse-tree-button" title="Collapse note tree" data-kb-action="CollapseTree" class="icon-action bx bx-layer-minus"></a>
<a id="scroll-to-active-note-button" title="Scroll to active note" data-kb-action="ScrollToActiveNote" class="icon-action bx bx-crosshair"></a>
<a id="toggle-search-button" title="Search in notes" data-kb-action="SearchNotes" class="icon-action bx bx-search"></a>
</div>
<div id="search-box">
<div class="form-group">
<div class="input-group">
<input name="search-text" id="search-text" class="form-control"
placeholder="Search text, labels" autocomplete="off">
<div class="input-group-append">
<button id="do-search-button" class="btn btn-sm icon-button bx bx-search" title="Search (enter)"></button>
</div>
</div>
</div>
<div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;">
<button id="save-search-button" class="btn btn-sm"
title="This will create new saved search note under active note.">
<span class="bx bx-save"></span> Save search</button>
<button id="close-search-button" class="btn btn-sm"><span class="bx bx-x"></span> Close search</button>
</div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<div style="display: flex; align-items: center; justify-content: space-evenly; flex-wrap: wrap;">
<button id="save-search-button" class="btn btn-sm"
title="This will create new saved search note under active note.">
<span class="bx bx-save"></span> Save search</button>
<button id="close-search-button" class="btn btn-sm"><span class="bx bx-x"></span> Close search</button>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
</div>
<div id="search-results">
<strong>Search results:</strong>
<% include center.ejs %>
<ul id="search-results-inner"></ul>
</div>
<div id="tree"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
<% include sidebar.ejs %>
</div>
<% include tabs.ejs %>
<% include dialogs/about.ejs %>
<% include dialogs/add_link.ejs %>
<% include dialogs/attributes.ejs %>
@@ -194,6 +210,7 @@
<% include dialogs/clone_to.ejs %>
<% include dialogs/move_to.ejs %>
<% include dialogs/backend_log.ejs %>
<% include dialogs/include_note.ejs %>
</div>
<script type="text/javascript">
@@ -204,7 +221,8 @@
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>'
csrfToken: '<%= csrfToken %>',
isDev: '<%= isDev %>'
};
window.appCssNoteIds = <%- JSON.stringify(appCssNoteIds) %>;
</script>
@@ -228,6 +246,8 @@
<script src="libraries/dayjs.min.js"></script>
<script src="libraries/split.min.js"></script>
<link href="stylesheets/themes.css" rel="stylesheet">
<link href="stylesheets/style.css" rel="stylesheet">
<link href="stylesheets/desktop.css" rel="stylesheet">

View File

@@ -13,5 +13,9 @@
title="Zoom Out"></button>
</div>
<div class="note-detail-book-help alert alert-warning">
This note of type Book doesn't have any child notes so there's nothing to display. See <a href="https://github.com/zadam/trilium/wiki/Book-note">wiki</a> for details.
</div>
<div class="note-detail-book-content"></div>
</div>

View File

@@ -0,0 +1,25 @@
<div id="include-note-dialog" class="modal mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Include note</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<form id="include-note-form">
<div class="modal-body">
<div class="form-group">
<label for="include-note-autocomplete">Note</label>
<div class="input-group">
<input id="include-note-autocomplete" class="form-control" placeholder="search for note by its name">
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Include note <kbd>enter</kbd></button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -97,7 +97,8 @@
sourceId: '<%= sourceId %>',
maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>,
instanceName: '<%= instanceName %>',
csrfToken: '<%= csrfToken %>'
csrfToken: '<%= csrfToken %>',
isDev: '<%= isDev %>'
};
</script>

View File

@@ -1,7 +1,6 @@
<div class="note-detail-sidebar hide-in-zen-mode" style="width: <%= sidebarWidthPercent %>%; min-width: <%= sidebarMinWidth %>px">
<div style="text-align: center; margin-bottom: 5px;">
<button class="hide-sidebar-button">hide sidebar <span class="bx bx-chevrons-right"></span></button>
</div>
<button id="hide-sidebar-button" class="btn btn-sm icon-button bx bx-chevrons-right hide-in-zen-mode" title="Hide sidebar"></button>
<button id="show-sidebar-button" class="btn btn-sm icon-button bx bx-chevrons-left hide-in-zen-mode" title="Show sidebar"></button>
<div class="note-detail-widget-container"></div>
<div id="right-pane" class="hide-in-zen-mode">
<div id="sidebar-container"></div>
</div>

View File

@@ -1,43 +0,0 @@
<div class="note-tab-row hide-in-hide-in-zen-mode">
<div class="note-tab-row-content"></div>
</div>
<div id="note-tab-container">
<div class="note-tab-content note-tab-content-template">
<div class="note-detail-content" style="width: <%= contentWidthPercent %>%">
<% include title.ejs %>
<div class="note-detail-script-area"></div>
<table class="note-detail-promoted-attributes"></table>
<div class="note-detail-component-wrapper">
<div class="note-detail-text note-detail-component">
<div class="note-detail-text-editor" tabindex="10000"></div>
</div>
<div class="note-detail-code note-detail-component">
<div class="note-detail-code-editor"></div>
</div>
<% include details/empty.ejs %>
<% include details/search.ejs %>
<% include details/render.ejs %>
<% include details/file.ejs %>
<% include details/image.ejs %>
<% include details/relation_map.ejs %>
<% include details/protected_session_password.ejs %>
<% include details/book.ejs %>
</div>
</div>
<% include sidebar.ejs %>
</div>
</div>

View File

@@ -65,8 +65,6 @@
<a class="dropdown-item show-note-info-button"><kbd data-kb-action="ShowNoteInfo"></kbd> Note info</a>
</div>
</div>
<button class="btn btn-sm icon-button bx bx-chevrons-left show-sidebar-button" title="Show sidebar"></button>
</div>
</div>
</div>