Compare commits

...

42 Commits

Author SHA1 Message Date
azivner
08e062ab34 release 0.24.4-beta 2018-11-21 23:47:09 +01:00
azivner
3a06493459 partial workaround for the broken in page search (next & previous don't work, but at least highlighting works) 2018-11-21 23:39:19 +01:00
azivner
8159564885 fix .desktop + allow to turn this feature off 2018-11-21 11:19:33 +01:00
azivner
8ce3c1a480 generate local .desktop file so it shows among apps in linux desktop environments 2018-11-21 11:01:03 +01:00
azivner
dbc93f4a79 mitigations for tooltip flickering 2018-11-20 23:50:19 +01:00
azivner
92ffe321aa smaller context menu 2018-11-20 22:55:07 +01:00
azivner
6cb7d0098e changed layout a little bit to fix broken children overview in the electron/chrome 2018-11-20 22:49:10 +01:00
azivner
bdcb4361b2 simplification of note autocomplete result ordering by depth #240 2018-11-20 22:22:26 +01:00
azivner
15366d37d7 ordering results in autocomplete based on depth, closes #240 2018-11-20 21:22:20 +01:00
azivner
acd001501b release 0.24.3-beta 2018-11-20 13:01:41 +01:00
azivner
0019865807 disabling broken in page search 2018-11-20 12:54:03 +01:00
azivner
137ffcc4e3 enable line wrap for code editor 2018-11-20 10:49:03 +01:00
azivner
585398ad5c fix bug when saving non-text notes 2018-11-19 23:11:36 +01:00
azivner
50401954d1 disable cache for attribute name autocomplete 2018-11-19 22:17:08 +01:00
azivner
32a9df8489 fixes (and refactoring) for "empty attribute name" check 2018-11-19 22:06:51 +01:00
azivner
5bf5d1cac4 attribute dialog doesn't allow to (attempt to) save relation without target note 2018-11-19 21:58:52 +01:00
azivner
3608857f25 small fix to demo export 2018-11-19 21:09:59 +01:00
azivner
16a1dc12df jsdoc now doesn't include date into files to avoid unnecessary noise 2018-11-19 21:03:43 +01:00
azivner
9c834229b9 release 0.24.2-beta 2018-11-19 17:17:08 +01:00
azivner
3fd45b15e7 set icon and app name, closes #201 2018-11-19 17:16:22 +01:00
azivner
f20ab45576 fix absolute image paths to relative paths as part of parsing links 2018-11-19 15:00:49 +01:00
azivner
77a89d85c8 fix "copy image to clipboard" button 2018-11-19 12:12:58 +01:00
azivner
30249a353e renamed "mirror" relation to "inverse" relation 2018-11-19 12:07:33 +01:00
azivner
eb9bae9010 updated relation map in demo document 2018-11-19 11:47:40 +01:00
azivner
0c7ae527c5 allow dragging only one note at a time 2018-11-19 11:19:56 +01:00
azivner
fef4705e2f center button changed to "reset pan & zoom" 2018-11-19 11:14:55 +01:00
azivner
568c2c997f possibility to export single code note as markdown. UI fix of disabled buttons 2018-11-19 09:54:33 +01:00
azivner
d6b5cd6ead support saving files and images in markdown tar export 2018-11-19 09:46:24 +01:00
azivner
00ce379962 excludeFromExport support also in OPML 2018-11-19 09:34:05 +01:00
azivner
b1ed022771 release 0.24.1-beta 2018-11-19 00:06:44 +01:00
azivner
ad6cb6ba34 mirror relation has to target source note 2018-11-19 00:06:04 +01:00
azivner
2e76de5f34 minor relation map fixes 2018-11-18 23:57:39 +01:00
azivner
4f23f2515a fix relation saving 2018-11-18 23:36:21 +01:00
azivner
8e16cc2326 fix prompt mutability (autocomplete on answer input field) 2018-11-18 23:31:31 +01:00
azivner
49bca04ebb fix doubling of notes after some actions 2018-11-18 23:01:48 +01:00
azivner
05e9669eaf fixed relation map sizing issue 2018-11-18 22:49:53 +01:00
azivner
62a250a7fc fixes for tooltip handling 2018-11-18 20:57:52 +01:00
azivner
8299524682 fixed cleanup of deleted notes for protected notes 2018-11-18 11:34:40 +01:00
azivner
7691a59977 imported notes can reference root 2018-11-18 11:19:50 +01:00
azivner
3db2f6784d fix DB migration and consistency issues 2018-11-18 10:20:06 +01:00
azivner
48684d0509 fixed app termination after unsuccessful migration for electron 2018-11-18 09:05:50 +01:00
azivner
1ee8d9fd93 sql console executes selected text if there's a selection instead of the whole content 2018-11-18 08:29:56 +01:00
83 changed files with 569 additions and 261 deletions

View File

@@ -1,4 +1,4 @@
FROM node:10.12.0
FROM node:10.13.0
RUN apt-get update && apt-get install -y nasm

View File

@@ -18,6 +18,7 @@ See other pictures in [screenshot tour](https://github.com/zadam/trilium/wiki/Sc
* Note [attributes](https://github.com/zadam/trilium/wiki/Attributes) can be used for note organization, querying and advanced [scripting](https://github.com/zadam/trilium/wiki/Scripts)
* [Synchronization](https://github.com/zadam/trilium/wiki/Synchronization) with self-hosted sync server
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) for visualizing notes and their relations
* [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* Scales well in both usability and performance upwards of 100 000 notes
* [Night theme](https://github.com/zadam/trilium/wiki/Themes)
@@ -31,10 +32,6 @@ Trilium is provided as either desktop application ([Electron](https://electronjs
* If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Server-installation).
* Currently only recent Chrome and Firefox are supported (tested) browsers.
## Status
Trilium is beta quality software. While it is reasonably feature complete and is tested by its author, it lacks proper testing by more users. It's not yet recommended for daily use, but testing and experimentation is encouraged.
## Documentation
[See wiki for complete list of documentation pages.](https://github.com/zadam/trilium/wiki/)

View File

@@ -7,7 +7,7 @@ fi
VERSION=$1
PKG_DIR=dist/trilium-linux-x64-server
NODE_VERSION=10.12.0
NODE_VERSION=10.13.0
rm -r $PKG_DIR
mkdir $PKG_DIR

View File

@@ -11,15 +11,21 @@ rm -r dist/*
echo "Rebuilding binaries for linux-ia32"
./node_modules/.bin/electron-rebuild --arch=ia32
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=ia32 --overwrite
./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
mv "./dist/Trilium Notes-linux-ia32" ./dist/trilium-linux-ia32
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=win32 --arch=x64 --overwrite --icon=src/public/images/app-icons/win/icon.ico
mv "./dist/Trilium Notes-win32-x64" ./dist/trilium-win32-x64
# we build x64 as second so that we keep X64 binaries in node_modules for local development and server build
echo "Rebuilding binaries for linux-x64"
./node_modules/.bin/electron-rebuild --arch=x64
./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
./node_modules/.bin/electron-packager . --out=dist --executable-name=trilium --platform=linux --arch=x64 --overwrite
mv "./dist/Trilium Notes-linux-x64" ./dist/trilium-linux-x64
echo "Copying required windows binaries"

Binary file not shown.

View File

@@ -1,3 +1,9 @@
-- first fix deleted status of existing images
UPDATE note_images SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);
-- we don't need set data to null because table is going to be dropped anyway and we want image size into attribute
UPDATE images SET isDeleted = 1 WHERE imageId NOT IN (SELECT imageId FROM note_images WHERE isDeleted = 0);
-- allow null for note content (for deleted notes)
CREATE TABLE IF NOT EXISTS "notes_mig" (
`noteId` TEXT NOT NULL,

View File

@@ -0,0 +1 @@
UPDATE attributes SET isDeleted = 1 WHERE noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1);

View File

@@ -0,0 +1 @@
UPDATE attributes SET isDeleted = 1 WHERE type = 'relation' AND value NOT IN (SELECT noteId FROM notes WHERE notes.isDeleted = 0);

View File

@@ -0,0 +1 @@
UPDATE attributes SET value = replace(value, 'mirrorRelation', 'inverseRelation') WHERE type = 'relation-definition';

View File

@@ -288,7 +288,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -730,7 +730,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -3814,7 +3814,7 @@ transactional by default.
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -511,7 +511,7 @@ Each note can have multiple (at least one) branches, meaning it can be placed in
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -216,7 +216,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -358,7 +358,7 @@ this is different concept than attribute/relation.</div>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -7297,7 +7297,7 @@ Cache is note instance scoped.
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -403,7 +403,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -311,7 +311,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -288,7 +288,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -75,7 +75,7 @@ module.exports = ApiToken;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -151,7 +151,7 @@ module.exports = Attribute;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -105,7 +105,7 @@ module.exports = Branch;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -93,7 +93,7 @@ module.exports = Entity;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -92,7 +92,7 @@ module.exports = Link;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -651,7 +651,7 @@ module.exports = Note;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -91,7 +91,7 @@ module.exports = NoteRevision;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -78,7 +78,7 @@ module.exports = Option;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -75,7 +75,7 @@ module.exports = RecentNote;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -594,7 +594,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -56,7 +56,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -278,7 +278,7 @@ module.exports = BackendScriptApi;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:27 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -719,7 +719,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -2846,7 +2846,7 @@ Internally this serializes the anonymous function into string and sends it to ba
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -279,7 +279,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -1316,7 +1316,7 @@ Its notable omission is the note content.</div>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -76,7 +76,7 @@ export default Branch;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -64,7 +64,7 @@ export default NoteFull;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -128,7 +128,7 @@ export default NoteShort;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -339,7 +339,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -56,7 +56,7 @@
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -271,7 +271,7 @@ export default FrontendScriptApi;</code></pre>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Thu Nov 15 2018 13:33:28 GMT+0100 (Central European Standard Time)
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a>
</footer>
<script> prettyPrint(); </script>

View File

@@ -6,6 +6,7 @@ const log = require('./src/services/log');
const cls = require('./src/services/cls');
const url = require("url");
const port = require('./src/services/port');
const appIconService = require('./src/services/app_icon');
const app = electron.app;
const globalShortcut = electron.globalShortcut;
@@ -13,6 +14,8 @@ const globalShortcut = electron.globalShortcut;
// Adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')();
appIconService.installLocalAppIcon();
// Prevent window being garbage collected
let mainWindow;
@@ -70,6 +73,8 @@ app.on('activate', () => {
});
app.on('ready', async () => {
app.setAppUserModelId('com.github.zadam.trilium');
mainWindow = await createMainWindow();
const result = globalShortcut.register('CommandOrControl+Alt+P', cls.wrap(async () => {

7
jsdoc-conf.json Normal file
View File

@@ -0,0 +1,7 @@
{
"templates": {
"default": {
"includeDate": false
}
}
}

8
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.23.1",
"version": "0.24.3-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -10821,9 +10821,9 @@
}
},
"ws": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.0.tgz",
"integrity": "sha512-H3dGVdGvW2H8bnYpIDc3u3LH8Wue3Qh+Zto6aXXFzvESkTVT6rAfKR6tR/+coaUvxs8yHtmNV0uioBF62ZGSTg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.1.2.tgz",
"integrity": "sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==",
"requires": {
"async-limiter": "~1.0.0"
}

View File

@@ -1,7 +1,8 @@
{
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.24.0-beta",
"version": "0.24.4-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {
@@ -13,12 +14,9 @@
},
"scripts": {
"start": "node ./src/www",
"test-electron": "xo",
"rebuild-electron": "electron-rebuild",
"start-electron": "electron . --disable-gpu",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version= --icon=src/public/app-icons/win/icon.ico",
"build-backend-docs": "jsdoc -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "jsdoc -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
"build-backend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/backend_api src/entities/*.js src/services/backend_script_api.js",
"build-frontend-docs": "jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/javascripts/entities/*.js src/public/javascripts/services/frontend_script_api.js",
"build-docs": "npm run build-backend-docs && npm run build-frontend-docs"
},
"dependencies": {
@@ -64,7 +62,7 @@
"tar-stream": "1.6.2",
"turndown": "5.0.1",
"unescape": "1.0.1",
"ws": "6.1.0",
"ws": "6.1.2",
"xml2js": "0.4.19"
},
"devDependencies": {

View File

@@ -72,7 +72,7 @@ function AttributesModel() {
attr.relationDefinition = (attr.type === 'relation-definition' && attr.value) ? attr.value : {
multiplicityType: "singlevalue",
mirrorRelation: "",
inverseRelation: "",
isPromoted: true
};
@@ -114,7 +114,7 @@ function AttributesModel() {
function isValid() {
for (let attributes = self.ownedAttributes(), i = 0; i < attributes.length; i++) {
if (self.isEmptyName(i)) {
if (self.isEmptyName(i) || self.isEmptyRelationTarget(i)) {
return false;
}
}
@@ -191,7 +191,7 @@ function AttributesModel() {
},
relationDefinition: {
multiplicityType: "singlevalue",
mirrorRelation: "",
inverseRelation: "",
isPromoted: true
}
}));
@@ -209,7 +209,35 @@ function AttributesModel() {
this.isEmptyName = function(index) {
const cur = self.ownedAttributes()[index]();
return cur.name.trim() === "" && !cur.isDeleted && (cur.attributeId !== "" || cur.labelValue !== "" || cur.relationValue);
if (cur.name.trim() || cur.isDeleted) {
return false;
}
if (cur.attributeId) {
// name is empty and attribute already exists so this is NO-GO
return true;
}
if (cur.type === 'relation-definition' || cur.type === 'label-definition') {
// for definitions there's no possible empty value so we always require name
return true;
}
if (cur.type === 'label' && cur.labelValue) {
return true;
}
if (cur.type === 'relation' && cur.relationValue) {
return true;
}
return false;
};
this.isEmptyRelationTarget = function(index) {
const cur = self.ownedAttributes()[index]();
return cur.type === "relation" && !cur.isDeleted && cur.name && !cur.relationValue;
};
this.getTargetAttribute = function(target) {

View File

@@ -1,6 +1,9 @@
const $dialog = $("#prompt-dialog");
const $question = $("#prompt-dialog-question");
const $answer = $("#prompt-dialog-answer");
const $dialogBody = $dialog.find(".modal-body");
let $question;
let $answer;
const $form = $("#prompt-dialog-form");
let resolve;
@@ -11,8 +14,21 @@ function ask({ message, defaultValue, shown }) {
shownCb = shown;
$question.text(message);
$answer.val(defaultValue || "");
$question = $("<label>")
.prop("for", "prompt-dialog-answer")
.text(message);
$answer = $("<input>")
.prop("type", "text")
.prop("id", "prompt-dialog-answer")
.addClass("form-control")
.val(defaultValue || "");
$dialogBody.empty().append(
$("<div>")
.addClass("form-group")
.append($question)
.append($answer));
$dialog.modal();

View File

@@ -50,7 +50,12 @@ async function execute(e) {
e.preventDefault();
e.stopPropagation();
const sqlQuery = codeEditor.getValue();
// execute the selected text or the whole content if there's no selection
let sqlQuery = codeEditor.getSelection();
if (!sqlQuery) {
sqlQuery = codeEditor.getValue();
}
const result = await server.post("sql/execute", {
query: sqlQuery

View File

@@ -15,6 +15,8 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
minLength: 0
}, [{
displayKey: 'name',
// disabling cache is important here because otherwise cache can stay intact when switching between attribute type which will lead to autocomplete displaying attribute names for incorrect attribute type
cache: false,
source: async (term, cb) => {
const type = typeof attributeType === "function" ? attributeType() : attributeType;

View File

@@ -103,7 +103,13 @@ if (utils.isElectron()) {
});
}
$("#export-note-to-markdown-button").click(() => exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single'));
$("#export-note-to-markdown-button").click(function () {
if ($(this).hasClass("disabled")) {
return;
}
exportService.exportSubtree(noteDetailService.getCurrentNoteId(), 'markdown-single')
});
treeService.showTree();

View File

@@ -10,15 +10,13 @@ const dragAndDropSetup = {
node.setSelected(true);
const selectedNodes = treeService.getSelectedNodes().map(node => {
return {
noteId: node.data.noteId,
title: node.title
}
});
// this is for dragging notes into relation map
data.dataTransfer.setData("text", JSON.stringify(selectedNodes));
// we allow to drag only one note at a time because it multi-drag conflicts with multiple single drags
// in UX and single drag is probably more useful
data.dataTransfer.setData("text", JSON.stringify({
noteId: node.data.noteId,
title: node.title
}));
// This function MUST be defined to enable dragging for the tree.
// Return false to cancel dragging of node.

View File

@@ -25,9 +25,21 @@ function registerEntrypoints() {
$("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
$("#show-note-revisions-button").click(function() {
if ($(this).hasClass("disabled")) {
return;
}
$("#show-source-button").click(noteSourceDialog.showDialog);
noteRevisionsDialog.showCurrentNoteRevisions();
});
$("#show-source-button").click(function() {
if ($(this).hasClass("disabled")) {
return;
}
noteSourceDialog.showDialog();
});
$("#recent-changes-button").click(recentChangesDialog.showDialog);
@@ -85,10 +97,17 @@ function registerEntrypoints() {
$(document).bind('keydown', 'ctrl+f', () => {
if (utils.isElectron()) {
const searchInPage = require('electron-in-page-search').default;
const remote = require('electron').remote;
const $searchWindowWebview = $(".electron-in-page-search-window");
$searchWindowWebview.show();
const inPageSearch = searchInPage(remote.getCurrentWebContents());
const searchInPage = require('electron-in-page-search').default;
const {remote} = require('electron');
const inPageSearch = searchInPage(remote.getCurrentWebContents(), {
searchWindowWebview: $searchWindowWebview[0],
//openDevToolsOfSearchWindow: true,
customCssPath: '/libraries/electron-in-page-search/default-style.css'
});
inPageSearch.openSearchWindow();

View File

@@ -92,7 +92,7 @@ function initNoteAutocomplete($el, options) {
$el.on('autocomplete:selected', (event, suggestion) => $el.setSelectedPath(suggestion.path));
$el.on('autocomplete:closed', () => {
if (!$el.val().trim()) {
$el.setSelectedPath("");
clearText($el);
}
});
}

View File

@@ -32,7 +32,10 @@ async function show() {
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true,
tabindex: 100
tabindex: 100,
// we linewrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap there's no horizontal scrollbar so no problem
lineWrapping: true
});
onNoteChange(noteDetailService.noteChanged);
@@ -43,7 +46,9 @@ async function show() {
const currentNote = noteDetailService.getCurrentNote();
// this needs to happen after the element is shown, otherwise the editor won't be refreshed
codeEditor.setValue(currentNote.content);
// CodeMirror breaks pretty badly on null so even though it shouldn't happen (guarded by consistency check)
// we provide fallback
codeEditor.setValue(currentNote.content || "");
const info = CodeMirror.findModeByMIME(currentNote.mime);

View File

@@ -5,6 +5,7 @@ import infoService from "./info.js";
import server from "./server.js";
const $component = $('#note-detail-image');
const $imageWrapper = $('#note-detail-image-wrapper');
const $imageView = $('#note-detail-image-view');
const $imageDownloadButton = $("#image-download");
@@ -39,10 +40,10 @@ function selectImage(element) {
}
$copyToClipboardButton.click(() => {
$component.attr('contenteditable','true');
$imageWrapper.attr('contenteditable','true');
try {
selectImage($component.get(0));
selectImage($imageWrapper.get(0));
const success = document.execCommand('copy');
@@ -55,7 +56,7 @@ $copyToClipboardButton.click(() => {
}
finally {
window.getSelection().removeAllRanges();
$component.removeAttr('contenteditable');
$imageWrapper.removeAttr('contenteditable');
}
});

View File

@@ -15,7 +15,7 @@ const $relationMapContainer = $("#relation-map-container");
const $createChildNote = $("#relation-map-create-child-note");
const $zoomInButton = $("#relation-map-zoom-in");
const $zoomOutButton = $("#relation-map-zoom-out");
const $centerButton = $("#relation-map-center");
const $resetPanZoomButton = $("#relation-map-reset-pan-zoom");
let mapData;
let jsPlumbInstance;
@@ -50,7 +50,7 @@ const biDirectionalOverlays = [
} ]
];
const mirrorOverlays = [
const inverseRelationsOverlays = [
[ "Arrow", {
location: 1,
id: "arrow",
@@ -117,6 +117,15 @@ async function show() {
}
function clearMap() {
// delete all endpoints and connections
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$relationMapContainer.empty();
}
async function loadNotesAndRelations() {
const noteIds = mapData.notes.map(note => note.noteId);
const data = await server.post("notes/relation-map", {noteIds});
@@ -125,12 +134,12 @@ async function loadNotesAndRelations() {
for (const relation of data.relations) {
const match = relations.find(rel =>
rel.name === data.mirrorRelations[relation.name]
rel.name === data.inverseRelations[relation.name]
&& ((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId)
|| (rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId)));
if (match) {
match.type = relation.type = relation.name === data.mirrorRelations[relation.name] ? 'biDirectional' : 'mirror';
match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? 'biDirectional' : 'inverse';
relation.render = false; // don't render second relation
} else {
relation.type = 'uniDirectional';
@@ -142,11 +151,9 @@ async function loadNotesAndRelations() {
mapData.notes = mapData.notes.filter(note => note.noteId in data.noteTitles);
// delete all endpoints and connections
// this is done at this point (after async operations) to reduce flicker to the minimum
jsPlumbInstance.deleteEveryEndpoint();
jsPlumbInstance.batch(async function () {
clearMap();
for (const note of mapData.notes) {
const title = data.noteTitles[note.noteId];
@@ -166,9 +173,9 @@ async function loadNotesAndRelations() {
connection.id = relation.attributeId;
if (relation.type === 'mirror') {
if (relation.type === 'inverse') {
connection.getOverlay("label-source").setLabel(relation.name);
connection.getOverlay("label-target").setLabel(data.mirrorRelations[relation.name]);
connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]);
}
else {
connection.getOverlay("label").setLabel(relation.name);
@@ -208,10 +215,17 @@ function initPanZoom() {
mapData.notes.push({ noteId: clipboard.noteId, x, y });
saveData();
clipboard = null;
}
return true;
},
filterKey: function(e, dx, dy, dz) {
// if ALT is pressed then panzoom should bubble the event up
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
return e.altKey;
}
});
@@ -226,6 +240,10 @@ function initPanZoom() {
pzInstance.moveTo(mapData.transform.x, mapData.transform.y);
}
else {
// set to initial coordinates
pzInstance.moveTo(0, 0);
}
$zoomInButton.click(() => pzInstance.zoomTo(0, 0, 1.2));
$zoomOutButton.click(() => pzInstance.zoomTo(0, 0, 0.8));
@@ -244,11 +262,7 @@ function saveCurrentTransform() {
function cleanup() {
if (jsPlumbInstance) {
// delete all endpoints and connections
jsPlumbInstance.deleteEveryEndpoint();
// without this we still end up with note boxes remaining in the canvas
$relationMapContainer.empty();
clearMap();
}
if (pzInstance) {
@@ -276,7 +290,7 @@ function initJsPlumbInstance () {
jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays });
jsPlumbInstance.registerConnectionType("mirror", { anchor:"Continuous", connector:"StateMachine", overlays: mirrorOverlays });
jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays });
jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays });
@@ -312,8 +326,6 @@ function connectionContextMenuHandler(connection, event) {
async function connectionCreatedHandler(info, originalEvent) {
const connection = info.connection;
const isRelation = relations.some(rel => rel.attributeId === connection.id);
connection.bind("contextmenu", (obj, event) => {
if (connection.getType().includes("link")) {
// don't create context menu if it's a link since there's nothing to do with link from relation map
@@ -362,9 +374,7 @@ async function connectionCreatedHandler(info, originalEvent) {
return;
}
const attribute = await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
relations.push({ attributeId: attribute.attributeId , targetNoteId, sourceNoteId, name });
await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
await refresh();
}
@@ -512,43 +522,20 @@ function getZoom() {
async function dropNoteOntoRelationMapHandler(ev) {
ev.preventDefault();
const notes = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
const note = JSON.parse(ev.originalEvent.dataTransfer.getData("text"));
let {x, y} = getMousePosition(ev);
// modifying position so that cursor is on the top-center of the box
const startX = x -= 80;
y -= 15;
const exists = mapData.notes.some(n => n.noteId === note.noteId);
const currentNoteId = treeService.getCurrentNode().data.noteId;
if (exists) {
await infoDialog.info(`Note "${note.title}" is already placed into the diagram`);
for (const note of notes) {
if (note.noteId === currentNoteId) {
// we don't allow placing current (relation map) into itself
// the reason is that when dragging notes from the tree, the relation map is always selected
// since it's focused.
continue;
}
const exists = mapData.notes.some(n => n.noteId === note.noteId);
if (exists) {
await infoDialog.info(`Note "${note.title}" is already placed into the diagram`);
continue;
}
mapData.notes.push({noteId: note.noteId, x, y});
if (x - startX > 1000) {
x = startX;
y += 200;
}
else {
x += 200;
}
return;
}
mapData.notes.push({noteId: note.noteId, x, y});
saveData();
await refresh();
@@ -565,40 +552,10 @@ function getMousePosition(evt) {
};
}
$centerButton.click(() => {
if (mapData.notes.length === 0) {
return; // nothing to recenter on
}
let totalX = 0, totalY = 0;
for (const note of mapData.notes) {
totalX += note.x;
totalY += note.y;
}
let averageX = totalX / mapData.notes.length;
let averageY = totalY / mapData.notes.length;
// find note with smallest X, Y difference from the average (most central note)
const {noteId} = mapData.notes.map(note => {
return {
noteId: note.noteId,
diff: Math.abs(note.x - averageX) + Math.abs(note.y - averageY)
}
}).reduce((min, val) => min.diff <= val.min ? min : val, { diff: 9999999999 });
const $noteBox = $("#" + noteIdToId(noteId));
const clientRect = $noteBox[0].getBoundingClientRect();
const cx = clientRect.left + clientRect.width / 2;
const cy = clientRect.top + clientRect.height / 2;
const container = $component[0].getBoundingClientRect();
const dx = container.width / 2 - cx;
const dy = container.height / 2 - cy;
pzInstance.moveBy(dx, dy, true);
$resetPanZoomButton.click(() => {
// reset to initial pan & zoom state
pzInstance.zoomTo(0, 0, 1 / getZoom());
pzInstance.moveTo(0, 0);
});
$component.on("drop", dropNoteOntoRelationMapHandler);

View File

@@ -41,6 +41,10 @@ function setupTooltip() {
if ($(this).is(":hover")) {
$(this).tooltip({
delay: {"show": 300, "hide": 100},
container: 'body',
placement: 'auto',
trigger: 'manual',
boundary: 'window',
title: html,
html: true
});
@@ -50,7 +54,7 @@ function setupTooltip() {
});
$(document).on("mouseleave", "a", function() {
$(this).tooltip('hide');
$(this).tooltip('dispose');
});
// close any tooltip after click, this fixes the problem that sometimes tooltips remained on the screen

View File

@@ -0,0 +1,57 @@
html, body {
margin: 0;
width: 100%;
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Meiryo", sans-serif;
overflow: hidden;
}
.inpage-search-body {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin: 8px;
padding: 10px;
border: solid #aaaaaa 1px;
border-radius: 10px;
background-color: #fafafa;
}
.inpage-search-input {
width: 200px;
}
.inpage-search-matches {
color: #999;
font-size: 0.8em;
}
.inpage-search-back {
margin-left: 2px;
padding-left: 6px;
padding-right: 2px;
cursor: pointer;
}
.inpage-search-forward {
padding-left: 2px;
padding-right: 6px;
cursor: pointer;
}
.inpage-search-close {
margin-left: 4px;
padding: 0 2px;
cursor: pointer;
}
.inpage-search-back:hover,
.inpage-search-forward:hover,
.inpage-search-close:hover {
background-color: #e2e0e2;
border-radius: 0.2em;
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes" />
<link href="/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="inpage-search-body">
<input class="inpage-search-input form-control form-control-sm" type="search" placeholder="Search..." autocomplete="off" autofocus/>
<div class="inpage-search-matches">0/0</div>
<div class="inpage-search-back" title="Previous result">&lt;</div>
<div class="inpage-search-forward" title="Next result">&gt;</div>
<div class="inpage-search-close" title="Close search"></div>
</div>
</body>
<script>var exports = {}</script>
</html>

View File

@@ -5,9 +5,7 @@
#relation-map-wrapper {
position: relative;
overflow: hidden !important;
height: 4000px; /* we need to set fixed dimentions. This number is probably enough to cover any screen */
width: 4000px;
height: 100%;
outline: none; /* remove dotted outline on click */
}

View File

@@ -76,9 +76,13 @@ body {
position: relative;
overflow: auto;
flex-basis: content;
height: 100%;
display: flex;
flex-direction: column;
}
.note-detail-component {
flex-grow: 100;
display: none;
}
@@ -210,12 +214,12 @@ div.ui-tooltip {
*/
.electron-in-page-search-window {
position: fixed;
top: 50px;
right: 0;
border: solid grey 1px;
background-color: white;
width: 300px;
height: 36px;
top: 45px;
right: 10px;
width: 360px;
height: 55px;
display: none;
z-index: 1001;
}
/*
@@ -343,11 +347,11 @@ div.ui-tooltip {
#children-overview {
flex-grow: 1000;
flex-shrink: 1000;
flex-basis: 0px;
flex-basis: 0;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
height: 100px;
height: 110px;
overflow: auto;
}
@@ -527,12 +531,12 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
padding: 20px;
}
.context-menu {
.context-menu-container {
font-size: small;
}
.context-menu .dropdown-item {
padding: 2px 10px 2px 10px;
#context-menu-container .dropdown-item {
padding: 0 7px 0 10px;
}
/* if modal height overflows, then only modal body scrolls */
@@ -541,6 +545,11 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
overflow-y: auto;
}
/* this should help with tooltip flickering */
.tooltip {
pointer-events: none;
}
.tooltip-inner {
background-color: #fbfbfb !important;
max-width: 400px;
@@ -557,6 +566,10 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th
max-height: 250px;
}
.tooltip-inner figure.image-style-side {
float: right;
}
.tooltip.show {
opacity: 1;
}

View File

@@ -95,7 +95,7 @@ async function updateNoteAttributes(req) {
attributeEntity.isInheritable = attribute.isInheritable;
attributeEntity.isDeleted = attribute.isDeleted;
if (attributeEntity.type === 'relation' && !attributeEntity.value.trim()) {
if (attributeEntity.type === 'relation' && !attribute.value.trim()) {
// relation should never have empty target
attributeEntity.isDeleted = true;
}

View File

@@ -117,8 +117,8 @@ async function getRelationMap(req) {
// noteId => title
noteTitles: {},
relations: [],
// relation name => mirror relation name
mirrorRelations: {},
// relation name => inverse relation name
inverseRelations: {},
links: []
};
@@ -143,8 +143,8 @@ async function getRelationMap(req) {
}; }));
for (const relationDefinition of await note.getRelationDefinitions()) {
if (relationDefinition.value.mirrorRelation) {
resp.mirrorRelations[relationDefinition.name] = relationDefinition.value.mirrorRelation;
if (relationDefinition.value.inverseRelation) {
resp.inverseRelations[relationDefinition.name] = relationDefinition.value.inverseRelation;
}
}
}

64
src/services/app_icon.js Normal file
View File

@@ -0,0 +1,64 @@
"use strict";
const path = require('path');
const {APP_PNG_ICON_DIR, ELECTRON_APP_ROOT_DIR} = require("./resource_dir");
const log = require("./log");
const os = require('os');
const fs = require('fs');
const config = require('./config');
const template = `[Desktop Entry]
Type=Application
Name=Trilium Notes
Icon=#APP_PNG_ICON_DIR#/128x128.png
Exec=#EXE_PATH#
Categories=Office
Terminal=false
`;
/**
* Installs .desktop icon into standard ~/.local/share/applications directory.
* We overwrite this file during every run as it might have been updated.
*/
function installLocalAppIcon() {
if (["win32", "darwin"].includes(os.platform()) || (config.General && config.General.noDesktopIcon)) {
return;
}
const desktopDir = path.resolve(os.homedir(), '.local/share/applications');
fs.stat(desktopDir, function (err, stats) {
if (err) {
// Directory doesn't exist so we won't attempt to create the .desktop file
return;
}
if (stats.isDirectory()) {
const desktopFilePath = path.resolve(desktopDir, "trilium-notes.desktop");
fs.writeFile(desktopFilePath, getDesktopFileContent(), function (err) {
if (err) {
log.error("Desktop icon installation to ~/.local/share/applications failed.");
}
});
}
});
}
function getDesktopFileContent() {
return template
.replace("#APP_PNG_ICON_DIR#", escapePath(APP_PNG_ICON_DIR))
.replace("#EXE_PATH#", escapePath(getExePath()));
}
function escapePath(path) {
return path.replace(" ", "\\ ");
}
function getExePath() {
return path.resolve(ELECTRON_APP_ROOT_DIR, 'trilium');
}
module.exports = {
installLocalAppIcon
};

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 116;
const APP_DB_VERSION = 119;
const SYNC_VERSION = 2;
module.exports = {

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-11-16T23:30:52+01:00", buildRevision: "90eb1b53fbe915c4658617772aea4347a107a722" };
module.exports = { buildDate:"2018-11-21T23:47:09+01:00", buildRevision: "3a064934598b70878f6da4c11c0ceb84ef18db57" };

View File

@@ -209,6 +209,16 @@ async function runAllChecks() {
AND type != 'relation-map'`,
"Note has invalid type", errorList);
await runCheck(`
SELECT
noteId
FROM
notes
WHERE
isDeleted = 0
AND content IS NULL`,
"Note content is null even though it is not deleted", errorList);
await runCheck(`
SELECT
parentNoteId
@@ -275,8 +285,9 @@ async function runAllChecks() {
LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0
LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0
WHERE
sourceNote.noteId IS NULL
OR targetNote.noteId IS NULL`,
links.isDeleted = 0
AND (sourceNote.noteId IS NULL
OR targetNote.noteId IS NULL)`,
"Link to source/target note link is broken", errorList);
await runSyncRowChecks("notes", "noteId", errorList);

View File

@@ -4,8 +4,20 @@ const sanitize = require("sanitize-filename");
const TurndownService = require('turndown');
async function exportSingleMarkdown(note, res) {
const turndownService = new TurndownService();
const markdown = turndownService.turndown(note.content);
if (note.type !== 'text' && note.type !== 'code') {
return [400, `Note type ${note.type} cannot be exported as single markdown file.`];
}
let markdown;
if (note.type === 'code') {
markdown = '```\n' + note.content + "\n```";
}
else if (note.type === 'text') {
const turndownService = new TurndownService();
markdown = turndownService.turndown(note.content);
}
const name = sanitize(note.title);
res.setHeader('Content-Disposition', 'file; filename="' + name + '.md"');

View File

@@ -25,7 +25,7 @@ async function exportToMarkdown(branch, res) {
return;
}
saveDataFile(childFileName, note);
saveNote(childFileName, note);
const childNotes = await note.getChildNotes();
@@ -40,11 +40,7 @@ async function exportToMarkdown(branch, res) {
return childFileName;
}
function saveDataFile(childFileName, note) {
if (note.type !== 'text' && note.type !== 'code') {
return;
}
function saveTextNote(childFileName, note) {
if (note.content.trim().length === 0) {
return;
}
@@ -65,6 +61,19 @@ async function exportToMarkdown(branch, res) {
pack.entry({name: childFileName + ".md", size: markdown.length}, markdown);
}
function saveFileNote(childFileName, note) {
pack.entry({name: childFileName, size: note.content.length}, note.content);
}
function saveNote(childFileName, note) {
if (note.type === 'text' || note.type === 'code') {
saveTextNote(childFileName, note);
}
else if (note.type === 'image' || note.type === 'file') {
saveFileNote(childFileName, note);
}
}
function saveDirectory(childFileName) {
pack.entry({name: childFileName, type: 'directory'});
}

View File

@@ -12,6 +12,11 @@ async function exportToOpml(branch, res) {
async function exportNoteInner(branchId) {
const branch = await repository.getBranch(branchId);
const note = await branch.getNote();
if (await note.hasLabel('excludeFromExport')) {
return;
}
const title = (branch.prefix ? (branch.prefix + ' - ') : '') + note.title;
const preparedTitle = prepareText(title);

View File

@@ -59,7 +59,7 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, chi
await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
});
async function processMirrorRelations(entityName, entity, handler) {
async function processInverseRelations(entityName, entity, handler) {
if (entityName === 'attributes' && entity.type === 'relation') {
const note = await entity.getNote();
const attributes = (await note.getAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
@@ -67,7 +67,7 @@ async function processMirrorRelations(entityName, entity, handler) {
for (const attribute of attributes) {
const definition = attribute.value;
if (definition.mirrorRelation && definition.mirrorRelation.trim()) {
if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = await entity.getTargetNote();
await handler(definition, note, targetNote);
@@ -77,13 +77,17 @@ async function processMirrorRelations(entityName, entity, handler) {
}
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => {
await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
// we need to make sure that also target's mirror attribute exists and if note, then create it
if (!await targetNote.hasRelation(definition.mirrorRelation)) {
await processInverseRelations(entityName, entity, async (definition, note, targetNote) => {
// we need to make sure that also target's inverse attribute exists and if note, then create it
// inverse attribute has to target our note as well
const hasInverseAttribute = (await targetNote.getRelations(definition.inverseRelation))
.some(attr => attr.value === note.noteId);
if (!hasInverseAttribute) {
await new Attribute({
noteId: targetNote.noteId,
type: 'relation',
name: definition.mirrorRelation,
name: definition.inverseRelation,
value: note.noteId,
isInheritable: entity.isInheritable
}).save();
@@ -94,16 +98,21 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity
});
eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => {
await processMirrorRelations(entityName, entity, async (definition, note, targetNote) => {
// if one mirror attribute is deleted then the other should be deleted as well
const relations = await targetNote.getRelations(definition.mirrorRelation);
await processInverseRelations(entityName, entity, async (definition, note, targetNote) => {
// if one inverse attribute is deleted then the other should be deleted as well
const relations = await targetNote.getRelations(definition.inverseRelation);
let deletedSomething = false;
for (const relation of relations) {
relation.isDeleted = true;
await relation.save();
if (relation.value === note.noteId) {
relation.isDeleted = true;
await relation.save();
deletedSomething = true;
}
}
if (relations.length > 0) {
if (deletedSomething) {
targetNote.invalidateAttributeCache();
}
});

View File

@@ -31,6 +31,11 @@ async function importTar(fileBuffer, parentNote) {
return "";
}
// we allow references to root and they don't need translation
if (origNoteId === 'root') {
return origNoteId;
}
if (!ctx.noteIdMap[origNoteId]) {
ctx.noteIdMap[origNoteId] = utils.newEntityId();
}

View File

@@ -4,6 +4,7 @@ const sqlInit = require('./sql_init');
const optionService = require('./options');
const fs = require('fs-extra');
const log = require('./log');
const utils = require('./utils');
const resourceDir = require('./resource_dir');
async function migrate() {
@@ -72,7 +73,7 @@ async function migrate() {
log.error("error during migration to version " + mig.dbVersion + ": " + e.stack);
log.error("migration failed, crashing hard"); // this is not very user friendly :-/
process.exit(1);
utils.crash();
}
finally {
// make sure foreign keys are enabled even if migration script disables them

View File

@@ -80,7 +80,7 @@ function findNotes(query) {
continue;
}
// for leaf note it doesn't matter if "archived" label inheritable or not
// for leaf note it doesn't matter if "archived" label is inheritable or not
if (noteId in archived) {
continue;
}
@@ -113,11 +113,28 @@ function findNotes(query) {
}
}
results.sort((a, b) => a.title < b.title ? -1 : 1);
// sort results by depth of the note. This is based on the assumption that more important results
// are closer to the note root.
results.sort((a, b) => {
if (a.pathArray.length === b.pathArray.length) {
return a.title < b.title ? -1 : 1;
}
highlightResults(results, allTokens);
return a.pathArray.length < b.pathArray.length ? -1 : 1;
});
return results;
const apiResults = results.slice(0, 200).map(res => {
return {
noteId: res.noteId,
branchId: res.branchId,
path: res.pathArray.join('/'),
title: res.titleArray.join(' / ')
};
});
highlightResults(apiResults, allTokens);
return apiResults;
}
function search(noteId, tokens, path, results) {
@@ -125,15 +142,14 @@ function search(noteId, tokens, path, results) {
const retPath = getSomePath(noteId, path);
if (retPath) {
const noteTitle = getNoteTitleForPath(retPath);
const thisNoteId = retPath[retPath.length - 1];
const thisParentNoteId = retPath[retPath.length - 2];
results.push({
noteId: thisNoteId,
branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`],
title: noteTitle,
path: retPath.join('/')
pathArray: retPath,
titleArray: getNoteTitleArrayForPath(retPath)
});
}
@@ -146,10 +162,6 @@ function search(noteId, tokens, path, results) {
}
for (const parentNoteId of parents) {
if (results.length >= 200) {
return;
}
// archived must be inheritable
if (archived[parentNoteId] === 1) {
continue;
@@ -192,12 +204,12 @@ function getNoteTitle(noteId, parentNoteId) {
return (prefix ? (prefix + ' - ') : '') + title;
}
function getNoteTitleForPath(path) {
function getNoteTitleArrayForPath(path) {
const titles = [];
if (path[0] === 'root') {
if (path.length === 1) {
return getNoteTitle('root');
return [ getNoteTitle('root') ];
}
else {
path = path.slice(1);
@@ -213,6 +225,12 @@ function getNoteTitleForPath(path) {
parentNoteId = noteId;
}
return titles;
}
function getNoteTitleForPath(path) {
const titles = getNoteTitleArrayForPath(path);
return titles.join(' / ');
}

View File

@@ -68,6 +68,10 @@ async function createNewNote(parentNoteId, noteData) {
noteData.type = noteData.type || parentNote.type;
noteData.mime = noteData.mime || parentNote.mime;
if (noteData.type === 'text' || noteData.type === 'code') {
noteData.content = noteData.content || "";
}
const note = await new Note({
noteId: noteData.noteId, // optionally can force specific noteId
title: noteData.title,
@@ -173,7 +177,7 @@ async function protectNoteRevisions(note) {
}
function findImageLinks(content, foundLinks) {
const re = /src="\/api\/images\/([a-zA-Z0-9]+)\//g;
const re = /src="[^"]*\/api\/images\/([a-zA-Z0-9]+)\//g;
let match;
while (match = re.exec(content)) {
@@ -182,11 +186,13 @@ function findImageLinks(content, foundLinks) {
targetNoteId: match[1]
});
}
return match;
// removing absolute references to server to keep it working between instances
return content.replace(/src="[^"]*\/api\/images\//g, 'src="/api/images/');
}
function findHyperLinks(content, foundLinks) {
const re = /href="#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g;
const re = /href="[^"]*#root[a-zA-Z0-9\/]*\/([a-zA-Z0-9]+)\/?"/g;
let match;
while (match = re.exec(content)) {
@@ -196,7 +202,8 @@ function findHyperLinks(content, foundLinks) {
});
}
return match;
// removing absolute references to server to keep it working between instances
return content.replace(/href="[^"]*#root/g, 'href="#root');
}
function findRelationMapLinks(content, foundLinks) {
@@ -210,19 +217,19 @@ function findRelationMapLinks(content, foundLinks) {
}
}
async function saveLinks(note) {
async function saveLinks(note, content) {
if (note.type !== 'text' && note.type !== 'relation-map') {
return;
return content;
}
const foundLinks = [];
if (note.type === 'text') {
findImageLinks(note.content, foundLinks);
findHyperLinks(note.content, foundLinks);
content = findImageLinks(content, foundLinks);
content = findHyperLinks(content, foundLinks);
}
else if (note.type === 'relation-map') {
findRelationMapLinks(note.content, foundLinks);
findRelationMapLinks(content, foundLinks);
}
else {
throw new Error("Unrecognized type " + note.type);
@@ -258,6 +265,8 @@ async function saveLinks(note) {
unusedLink.isDeleted = true;
await unusedLink.save();
}
return content;
}
async function saveNoteRevision(note) {
@@ -306,6 +315,8 @@ async function updateNote(noteId, noteUpdates) {
const noteTitleChanged = note.title !== noteUpdates.title;
noteUpdates.content = await saveLinks(note, noteUpdates.content);
note.title = noteUpdates.title;
note.setContent(noteUpdates.content);
note.isProtected = noteUpdates.isProtected;
@@ -315,8 +326,6 @@ async function updateNote(noteId, noteUpdates) {
await triggerNoteTitleChanged(note);
}
await saveLinks(note);
await protectNoteRevisions(note);
}
@@ -374,19 +383,12 @@ async function deleteNote(branch) {
async function cleanupDeletedNotes() {
const cutoffDate = new Date(new Date().getTime() - 48 * 3600 * 1000);
const notesForCleanup = await repository.getEntities("SELECT * FROM notes WHERE isDeleted = 1 AND content != '' AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
// it's better to not use repository for this because it will complain about saving protected notes
// out of protected session
for (const note of notesForCleanup) {
note.content = null;
await note.save();
}
await sql.execute("UPDATE notes SET content = NULL WHERE isDeleted = 1 AND content IS NOT NULL AND dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
const notesRevisionsForCleanup = await repository.getEntities("SELECT note_revisions.* FROM notes JOIN note_revisions USING(noteId) WHERE notes.isDeleted = 1 AND note_revisions.content != '' AND notes.dateModified <= ?", [dateUtils.dateStr(cutoffDate)]);
for (const noteRevision of notesRevisionsForCleanup) {
noteRevision.content = null;
await noteRevision.save();
}
await sql.execute("UPDATE note_revisions SET content = NULL WHERE note_revisions.content IS NOT NULL AND noteId IN (SELECT noteId FROM notes WHERE isDeleted = 1 AND notes.dateModified <= ?)", [dateUtils.dateStr(cutoffDate)]);
}
// first cleanup kickoff 5 minutes after startup

View File

@@ -4,7 +4,10 @@ const fs = require('fs');
const RESOURCE_DIR = path.resolve(__dirname, "../..");
// where "trilium" executable is
const ELECTRON_APP_ROOT_DIR = path.resolve(RESOURCE_DIR, "../..");
const DB_INIT_DIR = path.resolve(RESOURCE_DIR, "db");
const APP_PNG_ICON_DIR = path.resolve(RESOURCE_DIR, "src/public/images/app-icons/png");
if (!fs.existsSync(DB_INIT_DIR)) {
log.error("Could not find DB initialization directory: " + DB_INIT_DIR);
@@ -21,5 +24,7 @@ if (!fs.existsSync(MIGRATIONS_DIR)) {
module.exports = {
RESOURCE_DIR,
MIGRATIONS_DIR,
DB_INIT_DIR
DB_INIT_DIR,
ELECTRON_APP_ROOT_DIR,
APP_PNG_ICON_DIR
};

View File

@@ -118,6 +118,15 @@ function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function crash() {
if (isElectron()) {
require('electron').app.exit(1);
}
else {
process.exit(1);
}
}
module.exports = {
randomSecureToken,
randomString,
@@ -137,5 +146,6 @@ module.exports = {
stripTags,
intersection,
union,
escapeRegExp
escapeRegExp,
crash
};

View File

@@ -18,9 +18,9 @@
<% include image.ejs %>
<% include relation_map.ejs %>
</div>
<div id="children-overview"></div>
<div id="children-overview"></div>
</div>
<div id="attribute-list">
<button class="btn btn-sm show-attributes-button">Attributes:</button>

View File

@@ -22,5 +22,7 @@
<br/><br/>
<img id="note-detail-image-view" />
<div id="note-detail-image-wrapper">
<img id="note-detail-image-view" />
</div>
</div>

View File

@@ -7,9 +7,9 @@
</button>
<button type="button"
class="btn icon-button floating-button jam jam-align-center"
title="Re-center view on notes"
id="relation-map-center" style="right: 100px;"></button>
class="btn icon-button floating-button jam jam-crop"
title="Reset pan & zoom to initial coordinates and magnification"
id="relation-map-reset-pan-zoom" style="right: 100px;"></button>
<div class="btn-group floating-button" style="right: 20px;">
<button type="button"

View File

@@ -48,6 +48,10 @@
<input class="form-control relation-target-note-id"
placeholder="search for note by its name"
data-bind="noteAutocomplete, value: relationValue, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }">
<div style="color: red" data-bind="if: $parent.isEmptyRelationTarget($index())">Relation target note
can't be empty.
</div>
</div>
<div data-bind="visible: type == 'label-definition'">
@@ -72,9 +76,9 @@
</label>
<br/>
<label>
Mirror relation:
Inverse relation:
<input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.mirrorRelation"/>
<input type="text" value="true" class="attribute-name" data-bind="value: relationDefinition.inverseRelation"/>
</label>
</div>
</td>

View File

@@ -10,10 +10,6 @@
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="prompt-dialog-answer" id="prompt-dialog-question"></label>
<input type="text" class="form-control" id="prompt-dialog-answer" placeholder="">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary btn-sm" id="prompt-dialog-ok-button">OK <kbd>enter</kbd></button>

View File

@@ -159,7 +159,7 @@
<a class="dropdown-item show-attributes-button"><kbd>Alt+A</kbd> Attributes</a>
<a class="dropdown-item" id="show-source-button" data-bind="css: { disabled: type() != 'text' }">HTML source</a>
<a class="dropdown-item" id="upload-file-button">Upload file</a>
<a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' }">Export as markdown</a>
<a class="dropdown-item" id="export-note-to-markdown-button" data-bind="css: { disabled: type() != 'text' && type() != 'code' }">Export as markdown</a>
</div>
</div>
</div>
@@ -187,6 +187,8 @@
<% include dialogs/confirm.ejs %>
</div>
<webview class="electron-in-page-search-window" nodeintegration disablewebsecurity src="/libraries/electron-in-page-search/search-window.html"></webview>
<script type="text/javascript">
window.baseApiUrl = 'api/';
window.glob = {