Merge branch 'master' into next58

# Conflicts:
#	libraries/ckeditor/ckeditor.js
#	libraries/ckeditor/ckeditor.js.map
#	package-lock.json
#	package.json
#	src/public/app/layouts/desktop_layout.js
This commit is contained in:
zadam 2022-11-22 20:46:08 +01:00
commit 19126b7c6d
141 changed files with 17696 additions and 14852 deletions

View File

@ -2,7 +2,7 @@ image:
file: .gitpod.dockerfile file: .gitpod.dockerfile
tasks: tasks:
- before: nvm install 16.15.0 && nvm use 16.15.0 - before: nvm install 16.18.0 && nvm use 16.18.0
init: npm install init: npm install
command: npm run start-server command: npm run start-server

View File

@ -1,5 +1,5 @@
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!! # !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
FROM node:16.15.0-alpine FROM node:16.18.0-alpine
# Create app directory # Create app directory
WORKDIR /usr/src/app WORKDIR /usr/src/app
@ -27,6 +27,11 @@ RUN apk add --no-cache su-exec shadow
# Bundle app source # Bundle app source
COPY . . COPY . .
RUN sed -i -e 's/app\/desktop.js/app-dist\/desktop.js/g' src/views/desktop.ejs && \
sed -i -e 's/app\/mobile.js/app-dist\/mobile.js/g' src/views/mobile.ejs && \
sed -i -e 's/app\/setup.js/app-dist\/setup.js/g' src/views/setup.ejs && \
sed -i -e 's/app\/share.js/app-dist\/share.js/g' src/views/share/*.ejs
# Add application user and setup proper volume permissions # Add application user and setup proper volume permissions
RUN adduser -s /bin/false node; exit 0 RUN adduser -s /bin/false node; exit 0

View File

@ -23,7 +23,7 @@ Ukraine is currently defending itself from Russian aggression, please consider [
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://github.com/zadam/trilium/wiki/Sharing) (publishing) notes to public internet * [Sharing](https://github.com/zadam/trilium/wiki/Sharing) (publishing) notes to public internet
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) with per-note granularity * Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes) with per-note granularity
* Sketching diagrams with bult-in Excalidraw (note type "canvas") * Sketching diagrams with built-in Excalidraw (note type "canvas")
* [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) for visualizing notes and their relations * [Relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-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) * [Scripting](https://github.com/zadam/trilium/wiki/Scripts) - see [Advanced showcases](https://github.com/zadam/trilium/wiki/Advanced-showcases)
* [REST API](https://github.com/zadam/trilium/wiki/ETAPI) for automation * [REST API](https://github.com/zadam/trilium/wiki/ETAPI) for automation

View File

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

View File

@ -5,7 +5,7 @@ if [[ $# -eq 0 ]] ; then
exit 1 exit 1
fi fi
n exec 16.15.0 npm run webpack n exec 16.18.0 npm run webpack
DIR=$1 DIR=$1
@ -30,7 +30,7 @@ cp -r electron.js $DIR/
cp webpack-* $DIR/ cp webpack-* $DIR/
# run in subshell (so we return to original dir) # run in subshell (so we return to original dir)
(cd $DIR && n exec 16.15.0 npm install --only=prod) (cd $DIR && n exec 16.18.0 npm install --only=prod)
# cleanup of useless files in dependencies # cleanup of useless files in dependencies
rm -r $DIR/node_modules/image-q/demo rm -r $DIR/node_modules/image-q/demo

View File

@ -1,11 +1,6 @@
- drop branches.utcDateCreated - not used for anything
- drop options.utcDateCreated - not used for anything
- isDeleted = 0 by default - isDeleted = 0 by default
- rename openTabs to openNoteContexts - rename openTabs to openNoteContexts
- migrate black theme to dark theme
- unify readOnly handling to a single attribute: - unify readOnly handling to a single attribute:
* readOnly - like now * readOnly - like now
* readOnly=auto - like without readOnly (used to override inherited readOnly) * readOnly=auto - like without readOnly (used to override inherited readOnly)
* readOnly=never - like autoReadOnlyDisabled * readOnly=never - like autoReadOnlyDisabled
- remove focusOnAttributesKeyboardShortcut
- rename white theme to "light" theme (it's not completely white and matches well to dark theme)

View File

@ -991,7 +991,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -1904,7 +1904,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

File diff suppressed because it is too large Load Diff

View File

@ -1844,7 +1844,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -1461,7 +1461,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

File diff suppressed because it is too large Load Diff

View File

@ -2174,7 +2174,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -1319,7 +1319,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -1251,7 +1251,7 @@ This is a low level method, for notes and branches use `note.deleteNote()` and '
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -209,7 +209,7 @@ module.exports = AbstractEntity;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -254,7 +254,7 @@ module.exports = Attribute;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -198,6 +198,10 @@ class Branch extends AbstractEntity {
log.info("Deleting note " + note.noteId); log.info("Deleting note " + note.noteId);
// marking note as deleted as a signal to event handlers that the note is being deleted
// (isDeleted is being checked against becca)
delete this.becca.notes[note.noteId];
for (const attribute of note.getOwnedAttributes()) { for (const attribute of note.getOwnedAttributes()) {
attribute.markAsDeleted(deleteId); attribute.markAsDeleted(deleteId);
} }
@ -274,7 +278,7 @@ module.exports = Branch;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -120,7 +120,7 @@ module.exports = EtapiToken;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -38,7 +38,8 @@ const AbstractEntity = require("./abstract_entity");
const NoteRevision = require("./note_revision"); const NoteRevision = require("./note_revision");
const TaskContext = require("../../services/task_context"); const TaskContext = require("../../services/task_context");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc');
const searchService = require("../../services/search/services/search.js");
dayjs.extend(utc) dayjs.extend(utc)
const LABEL = 'label'; const LABEL = 'label';
@ -431,7 +432,7 @@ class Note extends AbstractEntity {
templateAttributes.push( templateAttributes.push(
...templateNote.__getAttributes(newPath) ...templateNote.__getAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited // template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' &amp;&amp; attr.name === 'template')) .filter(attr => !(attr.type === 'label' &amp;&amp; (attr.name === 'template' || attr.name === 'workspacetemplate')))
); );
} }
} }
@ -726,7 +727,7 @@ class Note extends AbstractEntity {
// this is done so that non-search &amp; non-archived paths are always explored as first when looking for note path // this is done so that non-search &amp; non-archived paths are always explored as first when looking for note path
sortParents() { sortParents() {
this.parentBranches.sort((a, b) => this.parentBranches.sort((a, b) =>
a.branchId.startsWith('virt-') a.branchId.startsWith('virt-') // FIXME: search virtual notes appear only in froca so this is probably not necessary
|| a.parentNote?.hasInheritableOwnedArchivedLabel() ? 1 : -1); || a.parentNote?.hasInheritableOwnedArchivedLabel() ? 1 : -1);
this.parents = this.parentBranches this.parents = this.parentBranches
@ -867,30 +868,94 @@ class Note extends AbstractEntity {
return Array.from(set); return Array.from(set);
} }
/** @returns {Note[]} */ /** @return {Note[]} */
getSubtreeNotes(includeArchived = true) { getSearchResultNotes() {
const noteSet = new Set(); if (this.type !== 'search') {
return [];
}
try {
const searchService = require("../../services/search/services/search");
const {searchResultNoteIds} = searchService.searchFromNote(this);
const becca = this.becca;
return searchResultNoteIds
.map(resultNoteId => becca.notes[resultNoteId])
.filter(note => !!note);
}
catch (e) {
log.error(`Could not resolve search note ${this.noteId}: ${e.message}`);
return [];
}
}
/**
* @returns {{notes: Note[], relationships: Array.&lt;{parentNoteId: string, childNoteId: string}>}}
*/
getSubtree({includeArchived = true, resolveSearch = false} = {}) {
const noteSet = new Set();
const relationships = []; // list of tuples parentNoteId -> childNoteId
function resolveSearchNote(searchNote) {
try {
for (const resultNote of searchNote.getSearchResultNotes()) {
addSubtreeNotesInner(resultNote, searchNote);
}
}
catch (e) {
log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`);
}
}
function addSubtreeNotesInner(note, parentNote = null) {
// share can be removed after 0.57 since it will be put under hidden
if (note.noteId === 'hidden' || note.noteId === 'share') {
return;
}
if (parentNote) {
// this needs to happen first before noteSet check to include all clone relationships
relationships.push({
parentNoteId: parentNote.noteId,
childNoteId: note.noteId
});
}
if (noteSet.has(note)) {
return;
}
function addSubtreeNotesInner(note) {
if (!includeArchived &amp;&amp; note.isArchived) { if (!includeArchived &amp;&amp; note.isArchived) {
return; return;
} }
noteSet.add(note); noteSet.add(note);
for (const childNote of note.children) { if (note.type === 'search') {
addSubtreeNotesInner(childNote); if (resolveSearch) {
resolveSearchNote(note);
}
}
else {
for (const childNote of note.children) {
addSubtreeNotesInner(childNote, note);
}
} }
} }
addSubtreeNotesInner(this); addSubtreeNotesInner(this);
return Array.from(noteSet); return {
notes: Array.from(noteSet),
relationships
};
} }
/** @returns {String[]} */ /** @returns {String[]} */
getSubtreeNoteIds(includeArchived = true) { getSubtreeNoteIds({includeArchived = true, resolveSearch = false} = {}) {
return this.getSubtreeNotes(includeArchived).map(note => note.noteId); return this.getSubtree({includeArchived, resolveSearch})
.notes
.map(note => note.noteId);
} }
getDescendantNoteIds() { getDescendantNoteIds() {
@ -1378,7 +1443,7 @@ module.exports = Note;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -235,7 +235,7 @@ module.exports = NoteRevision;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -89,7 +89,7 @@ module.exports = Option;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -77,7 +77,7 @@ module.exports = RecentNote;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -391,7 +391,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line208">line 208</a> <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line210">line 210</a>
</li></ul></dd> </li></ul></dd>
@ -579,7 +579,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line169">line 169</a> <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line171">line 171</a>
</li></ul></dd> </li></ul></dd>
@ -767,7 +767,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line229">line 229</a> <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line231">line 231</a>
</li></ul></dd> </li></ul></dd>
@ -1053,7 +1053,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <dd class="tag-source"><ul class="dummy"><li>
<a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line236">line 236</a> <a href="services_backend_script_api.js.html">services/backend_script_api.js</a>, <a href="services_backend_script_api.js.html#line238">line 238</a>
</li></ul></dd> </li></ul></dd>
@ -1089,7 +1089,7 @@
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -56,7 +56,7 @@
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -28,8 +28,6 @@
<header> <header>
</header> </header>
<article> <article>
@ -38,6 +36,50 @@
<dl class="details">
<dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li>
<a href="services_sql.js.html">services/sql.js</a>, <a href="services_sql.js.html#line3">line 3</a>
</li></ul></dd>
</dl>
</div> </div>
@ -1258,7 +1300,7 @@
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -42,6 +42,8 @@ const appInfo = require('./app_info');
const searchService = require('./search/services/search'); const searchService = require('./search/services/search');
const SearchContext = require("./search/search_context"); const SearchContext = require("./search/search_context");
const becca = require("../becca/becca"); const becca = require("../becca/becca");
const ws = require("./ws");
const SpacedUpdate = require("./spaced_update");
/** /**
* This is the main backend API interface for scripts. It's published in the local "api" object. * This is the main backend API interface for scripts. It's published in the local "api" object.
@ -316,12 +318,34 @@ function BackendScriptApi(currentNote, apiParams) {
}); });
}; };
this.logMessages = {};
this.logSpacedUpdates = {};
/** /**
* Log given message to trilium logs. * Log given message to trilium logs and log pane in UI
* *
* @param message * @param message
*/ */
this.log = message => log.info(message); this.log = message => {
log.info(message);
const {noteId} = this.startNote;
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
ws.sendMessageToAllClients({
type: 'api-log-messages',
noteId,
messages
});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
};
/** /**
* Returns root note of the calendar. * Returns root note of the calendar.
@ -336,6 +360,7 @@ function BackendScriptApi(currentNote, apiParams) {
* *
* @method * @method
* @param {string} date in YYYY-MM-DD format * @param {string} date in YYYY-MM-DD format
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
* @deprecated use getDayNote instead * @deprecated use getDayNote instead
*/ */
@ -346,6 +371,7 @@ function BackendScriptApi(currentNote, apiParams) {
* *
* @method * @method
* @param {string} date in YYYY-MM-DD format * @param {string} date in YYYY-MM-DD format
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
*/ */
this.getDayNote = dateNoteService.getDayNote; this.getDayNote = dateNoteService.getDayNote;
@ -354,6 +380,7 @@ function BackendScriptApi(currentNote, apiParams) {
* Returns today's day note. If such note doesn't exist, it is created. * Returns today's day note. If such note doesn't exist, it is created.
* *
* @method * @method
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
*/ */
this.getTodayNote = dateNoteService.getTodayNote; this.getTodayNote = dateNoteService.getTodayNote;
@ -363,7 +390,8 @@ function BackendScriptApi(currentNote, apiParams) {
* *
* @method * @method
* @param {string} date in YYYY-MM-DD format * @param {string} date in YYYY-MM-DD format
* @param {object} options - "startOfTheWeek" - either "monday" (default) or "sunday" * @param {object} [options] - "startOfTheWeek" - either "monday" (default) or "sunday"
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
*/ */
this.getWeekNote = dateNoteService.getWeekNote; this.getWeekNote = dateNoteService.getWeekNote;
@ -373,6 +401,7 @@ function BackendScriptApi(currentNote, apiParams) {
* *
* @method * @method
* @param {string} date in YYYY-MM format * @param {string} date in YYYY-MM format
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
*/ */
this.getMonthNote = dateNoteService.getMonthNote; this.getMonthNote = dateNoteService.getMonthNote;
@ -382,6 +411,7 @@ function BackendScriptApi(currentNote, apiParams) {
* *
* @method * @method
* @param {string} year in YYYY format * @param {string} year in YYYY format
* @param {Note} [rootNote] - specify calendar root note, normally leave empty to use default calendar
* @returns {Note|null} * @returns {Note|null}
*/ */
this.getYearNote = dateNoteService.getYearNote; this.getYearNote = dateNoteService.getYearNote;
@ -482,7 +512,7 @@ module.exports = BackendScriptApi;
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -404,7 +404,7 @@ module.exports = {
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -844,13 +844,13 @@ and relation (representing named relationship between source and target note)</d
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -1056,13 +1056,13 @@ parents.</div>
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

File diff suppressed because it is too large Load Diff

View File

@ -775,13 +775,13 @@
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

File diff suppressed because it is too large Load Diff

View File

@ -115,13 +115,13 @@ export default Attribute;
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -80,6 +80,12 @@ class Branch {
get toString() { get toString() {
return `Branch(branchId=${this.branchId})`; return `Branch(branchId=${this.branchId})`;
} }
get pojo() {
const pojo = {...this};
delete pojo.froca;
return pojo;
}
} }
export default Branch; export default Branch;
@ -93,13 +99,13 @@ export default Branch;
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -76,13 +76,13 @@ export default NoteComplement;
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -32,6 +32,7 @@ import ws from "../services/ws.js";
import options from "../services/options.js"; import options from "../services/options.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js";
const LABEL = 'label'; const LABEL = 'label';
const RELATION = 'relation'; const RELATION = 'relation';
@ -295,7 +296,7 @@ class NoteShort {
attrArrs.push( attrArrs.push(
templateNote.__getCachedAttributes(newPath) templateNote.__getCachedAttributes(newPath)
// template attr is used as a marker for templates, but it's not meant to be inherited // template attr is used as a marker for templates, but it's not meant to be inherited
.filter(attr => !(attr.type === 'label' &amp;&amp; attr.name === 'template')) .filter(attr => !(attr.type === 'label' &amp;&amp; (attr.name === 'template' || attr.name === 'workspacetemplate')))
); );
} }
} }
@ -452,6 +453,11 @@ class NoteShort {
} }
} }
getColorClass() {
const color = this.getLabelValue("color");
return cssClassManager.createClassForColor(color);
}
isFolder() { isFolder() {
return this.type === 'search' return this.type === 'search'
|| this.getFilteredChildBranches().length > 0; || this.getFilteredChildBranches().length > 0;
@ -858,13 +864,13 @@ export default NoteShort;
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -395,7 +395,7 @@
<dt class="tag-source">Source:</dt> <dt class="tag-source">Source:</dt>
<dd class="tag-source"><ul class="dummy"><li> <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#line124">line 124</a> <a href="services_frontend_script_api.js.html">services/frontend_script_api.js</a>, <a href="services_frontend_script_api.js.html#line125">line 125</a>
</li></ul></dd> </li></ul></dd>
@ -425,13 +425,13 @@
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -50,13 +50,13 @@
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -155,13 +155,13 @@
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -41,6 +41,7 @@ import appContext from "./app_context.js";
import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js"; import NoteContextAwareWidget from "../widgets/note_context_aware_widget.js";
import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js"; import NoteContextCachingWidget from "../widgets/note_context_caching_widget.js";
import BasicWidget from "../widgets/basic_widget.js"; import BasicWidget from "../widgets/basic_widget.js";
import SpacedUpdate from "./spaced_update.js";
/** /**
* This is the main frontend API interface for scripts. It's published in the local "api" object. * This is the main frontend API interface for scripts. It's published in the local "api" object.
@ -158,7 +159,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/ */
/** /**
* Adds new button the the plugin area. * Adds new button to the plugin area.
* *
* @param {ToolbarButtonOptions} opts * @param {ToolbarButtonOptions} opts
*/ */
@ -380,7 +381,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link * @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link * @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
* @param {boolean} [params.showNoteIcon=false] - show also note icon before the title * @param {boolean} [params.showNoteIcon=false] - show also note icon before the title
* @param {string} [title=] - custom link tile with note's title as default * @param {string} [params.title=] - custom link tile with note's title as default
*/ */
this.createNoteLink = linkService.createNoteLink; this.createNoteLink = linkService.createNoteLink;
@ -622,6 +623,33 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @returns {string} random string * @returns {string} random string
*/ */
this.randomString = utils.randomString; this.randomString = utils.randomString;
this.logMessages = {};
this.logSpacedUpdates = {};
/**
* Log given message to the log pane in UI
*
* @param message
*/
this.log = message => {
const {noteId} = this.startNote;
message = utils.now() + ": " + message;
console.log(`Script ${noteId}: ${message}`);
this.logMessages[noteId] = this.logMessages[noteId] || [];
this.logSpacedUpdates[noteId] = this.logSpacedUpdates[noteId] || new SpacedUpdate(() => {
const messages = this.logMessages[noteId];
this.logMessages[noteId] = [];
appContext.triggerEvent("apiLogMessages", {noteId, messages});
}, 100);
this.logMessages[noteId].push(message);
this.logSpacedUpdates[noteId].scheduleUpdate();
};
} }
export default FrontendScriptApi; export default FrontendScriptApi;
@ -635,13 +663,13 @@ export default FrontendScriptApi;
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -74,13 +74,13 @@ export default class CollapsibleWidget extends NoteContextAwareWidget {
</div> </div>
<nav> <nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="module.exports.html">exports</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul> <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Attribute.html">Attribute</a></li><li><a href="Branch.html">Branch</a></li><li><a href="FrontendScriptApi.html">FrontendScriptApi</a></li><li><a href="NoteComplement.html">NoteComplement</a></li><li><a href="NoteShort.html">NoteShort</a></li><li><a href="module.exports.html">exports</a></li></ul><h3>Global</h3><ul><li><a href="global.html#doRenderBody">doRenderBody</a></li></ul>
</nav> </nav>
<br class="clear"> <br class="clear">
<footer> <footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.0</a>
</footer> </footer>
<script> prettyPrint(); </script> <script> prettyPrint(); </script>

View File

@ -6,7 +6,7 @@ It is meant as a last resort solution when the standard mean to access your data
## Installation ## Installation
This tool requires node.js, testing has been done on 16.15.0, but it will probably work on other versions as well. This tool requires node.js, testing has been done on 16.18.0, but it will probably work on other versions as well.
``` ```
npm install npm install

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
const {app, globalShortcut} = require('electron'); const {app, globalShortcut, BrowserWindow} = require('electron');
const sqlInit = require('./src/services/sql_init'); const sqlInit = require('./src/services/sql_init');
const appIconService = require('./src/services/app_icon'); const appIconService = require('./src/services/app_icon');
const windowService = require('./src/services/window'); const windowService = require('./src/services/window');
@ -27,11 +27,19 @@ app.on('ready', async () => {
// if db is not initialized -> setup process // if db is not initialized -> setup process
// if db is initialized, then we need to wait until the migration process is finished // if db is initialized, then we need to wait until the migration process is finished
if (await sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
await sqlInit.dbReady; await sqlInit.dbReady;
await windowService.createMainWindow(app); await windowService.createMainWindow(app);
if (process.platform === 'darwin') {
app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await windowService.createMainWindow(app);
}
});
}
tray.createTray(); tray.createTray();
} }
else { else {

View File

@ -520,7 +520,174 @@
border-radius: 50%; border-radius: 50%;
} }
.bxs-balloon:before {
content: "\eb60";
}
.bxs-castle:before {
content: "\eb79";
}
.bxs-coffee-bean:before {
content: "\eb92";
}
.bxs-objects-horizontal-center:before {
content: "\ebab";
}
.bxs-objects-horizontal-left:before {
content: "\ebc4";
}
.bxs-objects-horizontal-right:before {
content: "\ebdd";
}
.bxs-objects-vertical-bottom:before {
content: "\ebf6";
}
.bxs-objects-vertical-center:before {
content: "\ef40";
}
.bxs-objects-vertical-top:before {
content: "\ef41";
}
.bxs-pear:before {
content: "\ef42";
}
.bxs-shield-minus:before {
content: "\ef43";
}
.bxs-shield-plus:before {
content: "\ef44";
}
.bxs-shower:before {
content: "\ef45";
}
.bxs-sushi:before {
content: "\ef46";
}
.bxs-universal-access:before {
content: "\ef47";
}
.bx-child:before {
content: "\ef48";
}
.bx-horizontal-left:before {
content: "\ef49";
}
.bx-horizontal-right:before {
content: "\ef4a";
}
.bx-objects-horizontal-center:before {
content: "\ef4b";
}
.bx-objects-horizontal-left:before {
content: "\ef4c";
}
.bx-objects-horizontal-right:before {
content: "\ef4d";
}
.bx-objects-vertical-bottom:before {
content: "\ef4e";
}
.bx-objects-vertical-center:before {
content: "\ef4f";
}
.bx-objects-vertical-top:before {
content: "\ef50";
}
.bx-rfid:before {
content: "\ef51";
}
.bx-shield-minus:before {
content: "\ef52";
}
.bx-shield-plus:before {
content: "\ef53";
}
.bx-shower:before {
content: "\ef54";
}
.bx-sushi:before {
content: "\ef55";
}
.bx-universal-access:before {
content: "\ef56";
}
.bx-vertical-bottom:before {
content: "\ef57";
}
.bx-vertical-top:before {
content: "\ef58";
}
.bxl-graphql:before {
content: "\ef59";
}
.bxl-typescript:before {
content: "\ef5a";
}
.bxs-color:before {
content: "\ef39";
}
.bx-reflect-horizontal:before {
content: "\ef3a";
}
.bx-reflect-vertical:before {
content: "\ef3b";
}
.bx-color:before {
content: "\ef3c";
}
.bxl-mongodb:before {
content: "\ef3d";
}
.bxl-postgresql:before {
content: "\ef3e";
}
.bxl-deezer:before {
content: "\ef3f";
}
.bxs-hard-hat:before {
content: "\ef2a";
}
.bxs-home-alt-2:before {
content: "\ef2b";
}
.bxs-cheese:before {
content: "\ef2c";
}
.bx-home-alt-2:before {
content: "\ef2d";
}
.bx-hard-hat:before {
content: "\ef2e";
}
.bx-cheese:before {
content: "\ef2f";
}
.bx-cart-add:before {
content: "\ef30";
}
.bx-cart-download:before {
content: "\ef31";
}
.bx-no-signal:before {
content: "\ef32";
}
.bx-signal-1:before {
content: "\ef33";
}
.bx-signal-2:before {
content: "\ef34";
}
.bx-signal-3:before {
content: "\ef35";
}
.bx-signal-4:before {
content: "\ef36";
}
.bx-signal-5:before {
content: "\ef37";
}
.bxl-xing:before {
content: "\ef38";
}
.bxl-meta:before { .bxl-meta:before {
content: "\ef27"; content: "\ef27";
} }
@ -2436,7 +2603,7 @@
content: "\eb5f"; content: "\eb5f";
} }
.bx-menu-alt-left:before { .bx-menu-alt-left:before {
content: "\eb60"; content: "\ef5b";
} }
.bx-menu-alt-right:before { .bx-menu-alt-right:before {
content: "\eb61"; content: "\eb61";
@ -2511,7 +2678,7 @@
content: "\eb78"; content: "\eb78";
} }
.bx-message-rounded-edit:before { .bx-message-rounded-edit:before {
content: "\eb79"; content: "\ef5c";
} }
.bx-message-rounded-error:before { .bx-message-rounded-error:before {
content: "\eb7a"; content: "\eb7a";
@ -2586,7 +2753,7 @@
content: "\eb91"; content: "\eb91";
} }
.bx-mobile-vibration:before { .bx-mobile-vibration:before {
content: "\eb92"; content: "\ef5d";
} }
.bx-money:before { .bx-money:before {
content: "\eb93"; content: "\eb93";
@ -2661,7 +2828,7 @@
content: "\ebaa"; content: "\ebaa";
} }
.bx-paper-plane:before { .bx-paper-plane:before {
content: "\ebab"; content: "\ef61";
} }
.bx-paragraph:before { .bx-paragraph:before {
content: "\ebac"; content: "\ebac";
@ -2736,7 +2903,7 @@
content: "\ebc3"; content: "\ebc3";
} }
.bx-pointer:before { .bx-pointer:before {
content: "\ebc4"; content: "\ef5e";
} }
.bx-poll:before { .bx-poll:before {
content: "\ebc5"; content: "\ebc5";
@ -2811,7 +2978,7 @@
content: "\ebdc"; content: "\ebdc";
} }
.bx-reply:before { .bx-reply:before {
content: "\ebdd"; content: "\ef5f";
} }
.bx-reply-all:before { .bx-reply-all:before {
content: "\ebde"; content: "\ebde";
@ -2886,7 +3053,7 @@
content: "\ebf5"; content: "\ebf5";
} }
.bx-screenshot:before { .bx-screenshot:before {
content: "\ebf6"; content: "\ef60";
} }
.bx-search:before { .bx-search:before {
content: "\ebf7"; content: "\ebf7";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

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

File diff suppressed because one or more lines are too long

1792
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "Trilium Notes", "productName": "Trilium Notes",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.56.1", "version": "0.57.1-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"bin": { "bin": {
@ -44,7 +44,7 @@
"debounce": "^1.2.1", "debounce": "^1.2.1",
"ejs": "3.1.8", "ejs": "3.1.8",
"electron-debug": "3.2.0", "electron-debug": "3.2.0",
"electron-dl": "3.4.0", "electron-dl": "3.4.1",
"electron-window-state": "5.0.3", "electron-window-state": "5.0.3",
"express": "4.18.2", "express": "4.18.2",
"express-partial-content": "1.0.2", "express-partial-content": "1.0.2",
@ -62,7 +62,7 @@
"is-svg": "4.3.2", "is-svg": "4.3.2",
"jimp": "0.16.2", "jimp": "0.16.2",
"joplin-turndown-plugin-gfm": "1.0.12", "joplin-turndown-plugin-gfm": "1.0.12",
"jsdom": "20.0.1", "jsdom": "20.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"node-abi": "3.28.0", "node-abi": "3.28.0",
@ -75,7 +75,7 @@
"rimraf": "3.0.2", "rimraf": "3.0.2",
"safe-compare": "1.1.4", "safe-compare": "1.1.4",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"sanitize-html": "2.7.2", "sanitize-html": "2.7.3",
"sax": "1.2.4", "sax": "1.2.4",
"semver": "7.3.8", "semver": "7.3.8",
"serve-favicon": "2.5.0", "serve-favicon": "2.5.0",
@ -85,18 +85,18 @@
"tmp": "0.2.1", "tmp": "0.2.1",
"turndown": "7.1.1", "turndown": "7.1.1",
"unescape": "1.0.1", "unescape": "1.0.1",
"ws": "8.9.0", "ws": "8.11.0",
"yauzl": "2.10.0" "yauzl": "2.10.0"
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "16.2.8", "electron": "16.2.8",
"electron-builder": "23.6.0", "electron-builder": "23.6.0",
"electron-packager": "17.0.0", "electron-packager": "17.1.1",
"electron-rebuild": "3.2.9", "electron-rebuild": "3.2.9",
"esm": "3.2.25", "esm": "3.2.25",
"jasmine": "4.4.0", "jasmine": "4.5.0",
"jsdoc": "3.6.11", "jsdoc": "4.0.0",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"rcedit": "3.0.1", "rcedit": "3.0.1",
"webpack": "5.74.0", "webpack": "5.74.0",

View File

@ -4,7 +4,7 @@ const Branch = require('../../src/becca/entities/branch');
const SearchContext = require('../../src/services/search/search_context'); const SearchContext = require('../../src/services/search/search_context');
const dateUtils = require('../../src/services/date_utils'); const dateUtils = require('../../src/services/date_utils');
const becca = require('../../src/becca/becca'); const becca = require('../../src/becca/becca');
const {NoteBuilder, findNoteByTitle, note} = require('./note_cache_mocking'); const {NoteBuilder, findNoteByTitle, note} = require('./becca_mocking.js');
describe("Search", () => { describe("Search", () => {
let rootNote; let rootNote;

View File

@ -1,4 +1,4 @@
const {note} = require('./note_cache_mocking'); const {note} = require('./becca_mocking.js');
const ValueExtractor = require('../../src/services/search/value_extractor'); const ValueExtractor = require('../../src/services/search/value_extractor');
const becca = require('../../src/becca/becca'); const becca = require('../../src/becca/becca');
const SearchContext = require("../../src/services/search/search_context"); const SearchContext = require("../../src/services/search/search_context");

View File

@ -10,6 +10,7 @@ const FileStore = require('session-file-store')(session);
const sessionSecret = require('./services/session_secret'); const sessionSecret = require('./services/session_secret');
const dataDir = require('./services/data_dir'); const dataDir = require('./services/data_dir');
const utils = require('./services/utils'); const utils = require('./services/utils');
const assetPath = require('./services/asset_path');
require('./services/handlers'); require('./services/handlers');
require('./becca/becca_loader'); require('./becca/becca_loader');
@ -34,14 +35,23 @@ app.use(express.json({limit: '500mb'}));
app.use(express.raw({limit: '500mb'})); app.use(express.raw({limit: '500mb'}));
app.use(express.urlencoded({extended: false})); app.use(express.urlencoded({extended: false}));
app.use(cookieParser()); app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public/root')));
app.use('/libraries', express.static(path.join(__dirname, '..', 'libraries'))); app.use(`/${assetPath}/app`, express.static(path.join(__dirname, 'public/app')));
app.use(`/${assetPath}/app-dist`, express.static(path.join(__dirname, 'public/app-dist')));
app.use(`/${assetPath}/fonts`, express.static(path.join(__dirname, 'public/fonts')));
app.use(`/assets/vX/fonts`, express.static(path.join(__dirname, 'public/fonts')));
app.use(`/${assetPath}/stylesheets`, express.static(path.join(__dirname, 'public/stylesheets')));
app.use(`/assets/vX/stylesheets`, express.static(path.join(__dirname, 'public/stylesheets')));
app.use(`/${assetPath}/libraries`, express.static(path.join(__dirname, '..', 'libraries')));
app.use(`/assets/vX/libraries`, express.static(path.join(__dirname, '..', 'libraries')));
// excalidraw-view mode in shared notes // excalidraw-view mode in shared notes
app.use('/node_modules/react/umd/react.production.min.js', express.static(path.join(__dirname, '..', 'node_modules/react/umd/react.production.min.js'))); app.use(`/${assetPath}/node_modules/react/umd/react.production.min.js`, express.static(path.join(__dirname, '..', 'node_modules/react/umd/react.production.min.js')));
app.use('/node_modules/react-dom/umd/react-dom.production.min.js', express.static(path.join(__dirname, '..', 'node_modules/react-dom/umd/react-dom.production.min.js'))); app.use(`/${assetPath}/node_modules/react-dom/umd/react-dom.production.min.js`, express.static(path.join(__dirname, '..', 'node_modules/react-dom/umd/react-dom.production.min.js')));
// expose whole dist folder since complete assets are needed in edit and share // expose whole dist folder since complete assets are needed in edit and share
app.use('/node_modules/@excalidraw/excalidraw/dist/', express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/'))); app.use(`/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
app.use('/images', express.static(path.join(__dirname, '..', 'images'))); app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/`, express.static(path.join(__dirname, '..', 'node_modules/@excalidraw/excalidraw/dist/')));
app.use(`/${assetPath}/images`, express.static(path.join(__dirname, '..', 'images')));
app.use(`/assets/vX/images`, express.static(path.join(__dirname, '..', 'images')));
const sessionParser = session({ const sessionParser = session({
secret: sessionSecret, secret: sessionSecret,
resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request. resave: false, // true forces the session to be saved back to the session store, even if the session was never modified during the request.

View File

@ -170,6 +170,10 @@ class Branch extends AbstractEntity {
log.info("Deleting note " + note.noteId); log.info("Deleting note " + note.noteId);
// marking note as deleted as a signal to event handlers that the note is being deleted
// (isDeleted is being checked against becca)
delete this.becca.notes[note.noteId];
for (const attribute of note.getOwnedAttributes()) { for (const attribute of note.getOwnedAttributes()) {
attribute.markAsDeleted(deleteId); attribute.markAsDeleted(deleteId);
} }

View File

@ -10,7 +10,8 @@ const AbstractEntity = require("./abstract_entity");
const NoteRevision = require("./note_revision"); const NoteRevision = require("./note_revision");
const TaskContext = require("../../services/task_context"); const TaskContext = require("../../services/task_context");
const dayjs = require("dayjs"); const dayjs = require("dayjs");
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc');
const searchService = require("../../services/search/services/search.js");
dayjs.extend(utc) dayjs.extend(utc)
const LABEL = 'label'; const LABEL = 'label';
@ -839,30 +840,94 @@ class Note extends AbstractEntity {
return Array.from(set); return Array.from(set);
} }
/** @returns {Note[]} */ /** @return {Note[]} */
getSubtreeNotes(includeArchived = true) { getSearchResultNotes() {
const noteSet = new Set(); if (this.type !== 'search') {
return [];
}
try {
const searchService = require("../../services/search/services/search");
const {searchResultNoteIds} = searchService.searchFromNote(this);
const becca = this.becca;
return searchResultNoteIds
.map(resultNoteId => becca.notes[resultNoteId])
.filter(note => !!note);
}
catch (e) {
log.error(`Could not resolve search note ${this.noteId}: ${e.message}`);
return [];
}
}
/**
* @returns {{notes: Note[], relationships: Array.<{parentNoteId: string, childNoteId: string}>}}
*/
getSubtree({includeArchived = true, resolveSearch = false} = {}) {
const noteSet = new Set();
const relationships = []; // list of tuples parentNoteId -> childNoteId
function resolveSearchNote(searchNote) {
try {
for (const resultNote of searchNote.getSearchResultNotes()) {
addSubtreeNotesInner(resultNote, searchNote);
}
}
catch (e) {
log.error(`Could not resolve search note ${searchNote?.noteId}: ${e.message}`);
}
}
function addSubtreeNotesInner(note, parentNote = null) {
// share can be removed after 0.57 since it will be put under hidden
if (note.noteId === 'hidden' || note.noteId === 'share') {
return;
}
if (parentNote) {
// this needs to happen first before noteSet check to include all clone relationships
relationships.push({
parentNoteId: parentNote.noteId,
childNoteId: note.noteId
});
}
if (noteSet.has(note)) {
return;
}
function addSubtreeNotesInner(note) {
if (!includeArchived && note.isArchived) { if (!includeArchived && note.isArchived) {
return; return;
} }
noteSet.add(note); noteSet.add(note);
for (const childNote of note.children) { if (note.type === 'search') {
addSubtreeNotesInner(childNote); if (resolveSearch) {
resolveSearchNote(note);
}
}
else {
for (const childNote of note.children) {
addSubtreeNotesInner(childNote, note);
}
} }
} }
addSubtreeNotesInner(this); addSubtreeNotesInner(this);
return Array.from(noteSet); return {
notes: Array.from(noteSet),
relationships
};
} }
/** @returns {String[]} */ /** @returns {String[]} */
getSubtreeNoteIds(includeArchived = true) { getSubtreeNoteIds({includeArchived = true, resolveSearch = false} = {}) {
return this.getSubtreeNotes(includeArchived).map(note => note.noteId); return this.getSubtree({includeArchived, resolveSearch})
.notes
.map(note => note.noteId);
} }
getDescendantNoteIds() { getDescendantNoteIds() {

View File

@ -78,6 +78,18 @@ import NoteRevisionsButton from "../widgets/buttons/note_revisions_button.js";
import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js"; import EditableCodeButtonsWidget from "../widgets/type_widgets/editable_code_buttons.js";
import ApiLogWidget from "../widgets/api_log.js"; import ApiLogWidget from "../widgets/api_log.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js"; import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import AppearanceOptions from "../widgets/dialogs/options/appearance.js";
import KeyboardShortcutsOptions from "../widgets/dialogs/options/shortcuts.js";
import TextNotesOptions from "../widgets/dialogs/options/text_notes.js";
import CodeNotesOptions from "../widgets/dialogs/options/code_notes.js";
import ImageOptions from "../widgets/dialogs/options/images.js";
import SpellcheckOptions from "../widgets/dialogs/options/spellcheck.js";
import PasswordOptions from "../widgets/dialogs/options/password.js";
import EtapiOptions from "../widgets/dialogs/options/etapi.js";
import BackupOptions from "../widgets/dialogs/options/backup.js";
import SyncOptions from "../widgets/dialogs/options/sync.js";
import OtherOptions from "../widgets/dialogs/options/other.js";
import AdvancedOptions from "../widgets/dialogs/options/advanced.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) { constructor(customWidgets) {
@ -120,6 +132,7 @@ export default class DesktopLayout {
new NoteWrapperWidget() new NoteWrapperWidget()
.child(new FlexContainer('row').class('title-row') .child(new FlexContainer('row').class('title-row')
.css("height", "50px") .css("height", "50px")
.css("min-height", "50px")
.css('align-items', "center") .css('align-items', "center")
.cssBlock('.title-row > * { margin: 5px; }') .cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteIconWidget()) .child(new NoteIconWidget())
@ -207,6 +220,19 @@ export default class DesktopLayout {
.child(new InfoDialog()) .child(new InfoDialog())
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()) .child(new PromptDialog())
.child(new OptionsDialog()); .child(new OptionsDialog()
.child(new AppearanceOptions())
.child(new KeyboardShortcutsOptions())
.child(new TextNotesOptions())
.child(new CodeNotesOptions())
.child(new ImageOptions())
.child(new SpellcheckOptions())
.child(new PasswordOptions())
.child(new EtapiOptions())
.child(new BackupOptions())
.child(new SyncOptions())
.child(new OtherOptions())
.child(new AdvancedOptions())
);
} }
} }

View File

@ -76,6 +76,10 @@ class ContextMenu {
addItems($parent, items) { addItems($parent, items) {
for (const item of items) { for (const item of items) {
if (!item) {
continue;
}
if (item.title === '----') { if (item.title === '----') {
$parent.append($("<div>").addClass("dropdown-divider")); $parent.append($("<div>").addClass("dropdown-divider"));
} else { } else {

View File

@ -133,6 +133,7 @@ export default class TreeContextMenu {
this.treeWidget.triggerCommand(command, { this.treeWidget.triggerCommand(command, {
node: this.node, node: this.node,
notePath: notePath, notePath: notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node), selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node) selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
}); });

View File

@ -7,6 +7,7 @@ import Component from "../widgets/component.js";
import toastService from "./toast.js"; import toastService from "./toast.js";
import ws from "./ws.js"; import ws from "./ws.js";
import bundleService from "./bundle.js"; import bundleService from "./bundle.js";
import froca from "./froca.js";
export default class Entrypoints extends Component { export default class Entrypoints extends Component {
constructor() { constructor() {
@ -46,14 +47,15 @@ export default class Entrypoints extends Component {
appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true}); appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true});
} }
async toggleNoteHoistingCommand() { async toggleNoteHoistingCommand({noteId = appContext.tabManager.getActiveContextNoteId()}) {
const noteContext = appContext.tabManager.getActiveContext(); const noteToHoist = await froca.getNote(noteId);
const activeNoteContext = appContext.tabManager.getActiveContext();
if (noteContext.note.noteId === noteContext.hoistedNoteId) { if (noteToHoist.noteId === activeNoteContext.hoistedNoteId) {
await noteContext.unhoist(); await activeNoteContext.unhoist();
} }
else if (noteContext.note.type !== 'search') { else if (noteToHoist.type !== 'search') {
await noteContext.setHoistedNoteId(noteContext.note.noteId); await activeNoteContext.setHoistedNoteId(noteId);
} }
} }
@ -199,4 +201,12 @@ export default class Entrypoints extends Component {
activeContextChangedEvent() { activeContextChangedEvent() {
this.hideAllTooltips(); this.hideAllTooltips();
} }
async forceSaveNoteRevisionCommand() {
const noteId = appContext.tabManager.getActiveContextNoteId();
await server.post(`notes/${noteId}/revision`);
toastService.showMessage("Note revision has been created.");
}
} }

View File

@ -131,7 +131,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/ */
/** /**
* Adds new button the the plugin area. * Adds new button to the plugin area.
* *
* @param {ToolbarButtonOptions} opts * @param {ToolbarButtonOptions} opts
*/ */
@ -353,7 +353,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
* @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link * @param {boolean} [params.showTooltip=true] - enable/disable tooltip on the link
* @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link * @param {boolean} [params.showNotePath=false] - show also whole note's path as part of the link
* @param {boolean} [params.showNoteIcon=false] - show also note icon before the title * @param {boolean} [params.showNoteIcon=false] - show also note icon before the title
* @param {string} [title=] - custom link tile with note's title as default * @param {string} [params.title=] - custom link tile with note's title as default
*/ */
this.createNoteLink = linkService.createNoteLink; this.createNoteLink = linkService.createNoteLink;

View File

@ -64,7 +64,7 @@ function setupGlobs() {
}; };
for (const appCssNoteId of glob.appCssNoteIds || []) { for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`); libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false);
} }
utils.initHelpButtons($(window)); utils.initHelpButtons($(window));

View File

@ -86,6 +86,8 @@ async function requireLibrary(library) {
const loadedScriptPromises = {}; const loadedScriptPromises = {};
async function requireScript(url) { async function requireScript(url) {
url = window.glob.assetPath + "/" + url;
if (!loadedScriptPromises[url]) { if (!loadedScriptPromises[url]) {
loadedScriptPromises[url] = $.ajax({ loadedScriptPromises[url] = $.ajax({
url: url, url: url,
@ -97,12 +99,16 @@ async function requireScript(url) {
await loadedScriptPromises[url]; await loadedScriptPromises[url];
} }
async function requireCss(url) { async function requireCss(url, prependAssetPath = true) {
const cssLinks = Array const cssLinks = Array
.from(document.querySelectorAll('link')) .from(document.querySelectorAll('link'))
.map(el => el.href); .map(el => el.href);
if (!cssLinks.some(l => l.endsWith(url))) { if (!cssLinks.some(l => l.endsWith(url))) {
if (prependAssetPath) {
url = window.glob.assetPath + "/" + url;
}
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
} }
} }

View File

@ -17,6 +17,12 @@ async function createNoteLink(notePath, options = {}) {
return $("<span>").text("[missing note]"); return $("<span>").text("[missing note]");
} }
if (!notePath.startsWith("root")) {
// all note paths should start with "root/" (except for "root" itself)
// used e.g. to find internal links
notePath = "root/" + notePath;
}
let noteTitle = options.title; let noteTitle = options.title;
const showTooltip = options.showTooltip === undefined ? true : options.showTooltip; const showTooltip = options.showTooltip === undefined ? true : options.showTooltip;
const showNotePath = options.showNotePath === undefined ? false : options.showNotePath; const showNotePath = options.showNotePath === undefined ? false : options.showNotePath;
@ -97,8 +103,10 @@ function goToLink(e) {
const notePath = getNotePathFromLink($link); const notePath = getNotePathFromLink($link);
const ctrlKey = (!utils.isMac() && e.ctrlKey) || (utils.isMac() && e.metaKey);
if (notePath) { if (notePath) {
if ((e.which === 1 && e.ctrlKey) || e.which === 2) { if ((e.which === 1 && ctrlKey) || e.which === 2) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath); appContext.tabManager.openTabWithNoteWithHoisting(notePath);
} }
else if (e.which === 1) { else if (e.which === 1) {
@ -116,7 +124,7 @@ function goToLink(e) {
} }
} }
else { else {
if ((e.which === 1 && e.ctrlKey) || e.which === 2 if ((e.which === 1 && ctrlKey) || e.which === 2
|| $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices
|| $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices
) { ) {

View File

@ -33,26 +33,7 @@ async function getRenderedContent(note, options = {}) {
} }
} }
else { else {
$renderedContent.css("padding", "10px"); await renderChildrenList($renderedContent, note);
$renderedContent.addClass("text-with-ellipsis");
let childNoteIds = note.getChildNoteIds();
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
$renderedContent.append(await linkService.createNoteLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
}));
$renderedContent.append("<br>");
}
} }
} }
else if (type === 'code') { else if (type === 'code') {
@ -164,6 +145,9 @@ async function getRenderedContent(note, options = {}) {
$renderedContent.append($("<div>").text("Error parsing content. Please check console.error() for more details.")); $renderedContent.append($("<div>").text("Error parsing content. Please check console.error() for more details."));
} }
} }
else if (type === 'book') {
// nothing, book doesn't have its own content
}
else if (!options.tooltip && type === 'protected-session') { else if (!options.tooltip && type === 'protected-session') {
const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`) const $button = $(`<button class="btn btn-sm"><span class="bx bx-log-in"></span> Enter protected session</button>`)
.on('click', protectedSessionService.enterProtectedSession); .on('click', protectedSessionService.enterProtectedSession);
@ -187,6 +171,29 @@ async function getRenderedContent(note, options = {}) {
}; };
} }
async function renderChildrenList($renderedContent, note) {
$renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis");
let childNoteIds = note.getChildNoteIds();
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
$renderedContent.append(await linkService.createNoteLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,
showNoteIcon: true
}));
$renderedContent.append("<br>");
}
}
function trim(text, doTrim) { function trim(text, doTrim) {
if (!doTrim) { if (!doTrim) {
return text; return text;

View File

@ -230,18 +230,7 @@ function focusSavedElement() {
return; return;
} }
if ($lastFocusedElement.hasClass("ck")) { $lastFocusedElement.focus();
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
const editor = $lastFocusedElement
.closest('.ck-editor__editable')
.prop('ckeditorInstance');
editor.editing.view.focus();
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null; $lastFocusedElement = null;
} }
@ -252,10 +241,12 @@ async function openDialog($dialog, closeActDialog = true) {
} }
saveFocusedElement(); saveFocusedElement();
-
$dialog.modal(); $dialog.modal();
$dialog.on('hidden.bs.modal', () => { $dialog.on('hidden.bs.modal', () => {
$(".aa-input").autocomplete("close");
if (!glob.activeDialog || glob.activeDialog === $dialog) { if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement(); focusSavedElement();
} }

View File

@ -223,6 +223,7 @@ const ATTR_HELP = {
"shareRaw": "note will be served in its raw format, without HTML wrapper", "shareRaw": "note will be served in its raw format, without HTML wrapper",
"shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`, "shareDisallowRobotIndexing": `will forbid robot indexing of this note via <code>X-Robots-Tag: noindex</code> header`,
"shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.", "shareCredentials": "require credentials to access this shared note. Value is expected to be in format 'username:password'. Don't forget to make this inheritable to apply to child-notes/images.",
"shareIndex": "note with this this label will list all roots of shared notes",
"displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.", "displayRelations": "comma delimited names of relations which should be displayed. All other ones will be hidden.",
"hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.", "hideRelations": "comma delimited names of relations which should be hidden. All other ones will be displayed.",
"titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string "titleTemplate": `default title of notes created as children of this note. The value is evaluated as JavaScript string

View File

@ -16,7 +16,7 @@ const TPL = `
} }
.global-menu-button { .global-menu-button {
background-image: url("images/icon-black.png"); background-image: url("${window.glob.assetPath}/images/icon-black.png");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 45%; background-position: 50% 45%;
width: 100%; width: 100%;
@ -26,7 +26,7 @@ const TPL = `
} }
.global-menu-button:hover { .global-menu-button:hover {
background-image: url("images/icon-color.png"); background-image: url("${window.glob.assetPath}/images/icon-color.png");
} }
.global-menu-button-update-available { .global-menu-button-update-available {

View File

@ -20,7 +20,7 @@ const TPL = `
<label> <label>
<input class="delete-all-clones" value="1" type="checkbox"> <input class="delete-all-clones" value="1" type="checkbox">
delete also all clones delete also all clones (can be undone in recent changes)
</label> </label>
</div> </div>
@ -28,7 +28,7 @@ const TPL = `
<label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediatelly and it won't be possible to undelete the notes."> <label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediatelly and it won't be possible to undelete the notes.">
<input class="erase-notes" value="1" type="checkbox"> <input class="erase-notes" value="1" type="checkbox">
erase notes permanently (can't be undone). This will force application reload. erase notes permanently (can't be undone), including all clones. This will force application reload.
</label> </label>
</div> </div>

View File

@ -15,6 +15,20 @@ const TPL = `
overflow-y: auto; overflow-y: auto;
max-height: 85vh; max-height: 85vh;
} }
.options-dialog .options-section:first-of-type h4 {
margin-top: 0;
}
.options-dialog .options-section h4 {
margin-top: 15px;
margin-bottom: 15px;
}
.options-dialog .options-section h5 {
margin-top: 10px;
margin-bottom: 10px;
}
</style> </style>
<div class="modal-dialog modal-lg" style="min-width: 1000px;" role="document"> <div class="modal-dialog modal-lg" style="min-width: 1000px;" role="document">
@ -27,51 +41,9 @@ const TPL = `
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div style="display: flex"> <div style="display: flex">
<ul class="nav nav-tabs flex-column"> <ul class="nav nav-tabs flex-column"></ul>
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" href="#options-appearance">Appearance</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-shortcuts">Shortcuts</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-text-notes">Text notes</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-code-notes">Code notes</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-password">Password</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-etapi">ETAPI</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-backup">Backup</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-sync-setup">Sync</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-other">Other</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#options-advanced">Advanced</a>
</li>
</ul>
<br/> <br/>
<div class="tab-content"> <div class="tab-content"></div>
<div id="options-appearance" class="tab-pane active"></div>
<div id="options-shortcuts" class="tab-pane"></div>
<div id="options-text-notes" class="tab-pane"></div>
<div id="options-code-notes" class="tab-pane"></div>
<div id="options-password" class="tab-pane"></div>
<div id="options-etapi" class="tab-pane"></div>
<div id="options-backup" class="tab-pane"></div>
<div id="options-sync-setup" class="tab-pane"></div>
<div id="options-other" class="tab-pane"></div>
<div id="options-advanced" class="tab-pane"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -82,34 +54,51 @@ const TPL = `
export default class OptionsDialog extends BasicWidget { export default class OptionsDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$navTabs = this.$widget.find(".nav-tabs");
this.$tabContent = this.$widget.find(".tab-content");
for (const child of this.children) {
this.$navTabs.append(
$('<li class="nav-item">')
.append(
$('<a class="nav-link" data-toggle="tab">')
.attr("href", '#options-' + child.constructor.name)
.text(child.tabTitle)
)
);
this.$tabContent.append(
$('<div class="tab-pane">')
.attr("id", "options-" + child.constructor.name)
);
}
} }
async showOptionsEvent({openTab}) { async showOptionsEvent({openTab}) {
const options = await server.get('options'); const optionPromise = server.get('options');
utils.openDialog(this.$widget); for (const child of this.children) {
child.lazyRender();
(await Promise.all([ this.$widget.find("#options-" + child.constructor.name)
import('./options/appearance.js'), .empty()
import('./options/shortcuts.js'), .append(child.$widget);
import('./options/text_notes.js'),
import('./options/code_notes.js'),
import('./options/password.js'),
import('./options/etapi.js'),
import('./options/backup.js'),
import('./options/sync.js'),
import('./options/other.js'),
import('./options/advanced.js')
]))
.map(m => new m.default)
.forEach(tab => {
if (tab.optionsLoaded) {
tab.optionsLoaded(options)
}
});
if (openTab) {
$(`.nav-link[href='#options-${openTab}']`).trigger("click");
} }
const options = await optionPromise;
for (const child of this.children) {
if (child.optionsLoaded) {
child.optionsLoaded(options)
}
}
await utils.openDialog(this.$widget);
if (!openTab) {
openTab = "AppearanceOptions";
}
this.$widget.find(`.nav-link[href='#options-${openTab}']`).trigger("click");
} }
} }

View File

@ -1,62 +1,69 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4 style="margin-top: 0;">Sync</h4> <div class="options-section">
<button id="force-full-sync-button" class="btn">Force full sync</button> <h4>Sync</h4>
<button id="force-full-sync-button" class="btn">Force full sync</button>
<br/>
<br/>
<button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
<br/>
<br/>
<h4>Database integrity check</h4>
<p>This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.</p>
<button id="check-integrity-button" class="btn">Check database integrity</button><br/><br/>
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button><br/><br/>
<h4>Anonymize database</h4>
<h5>Full anonymization</h5>
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button><br/><br/>
<h5>Light anonymization</h5> <button id="fill-entity-changes-button" class="btn">Fill entity changes records</button>
</div>
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p> <div class="options-section">
<h4>Database integrity check</h4>
<p>This will check that the database is not corrupted on the SQLite level. It might take some time, depending on the DB size.</p>
<button id="check-integrity-button" class="btn">Check database integrity</button>
</div>
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p> <div class="options-section">
<h4>Consistency checks</h4>
<button id="find-and-fix-consistency-issues-button" class="btn">Find and fix consistency issues</button>
</div>
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button><br/><br/> <div class="options-section">
<h4>Anonymize database</h4>
<h5>Full anonymization</h5>
<p>This action will create a new copy of the database and anonymize it (remove all note content and leave only structure and some non-sensitive metadata)
for sharing online for debugging purposes without fear of leaking your personal data.</p>
<button id="anonymize-full-button" class="btn">Save fully anonymized database</button>
<h4>Vacuum database</h4> <h5>Light anonymization</h5>
<p>This action will create a new copy of the database and do a light anonymization on it - specifically only content of all notes will be removed, but titles and attributes will remain. Additionally, custom JS frontend/backend script notes and custom widgets will remain. This provides more context to debug the issues.</p>
<p>You can decide yourself if you want to provide fully or lightly anonymized database. Even fully anonymized DB is very useful, however in some cases lightly anonymized database can speed up the process of bug identification and fixing.</p>
<button id="anonymize-light-button" class="btn">Save lightly anonymized database</button>
</div>
<p>This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.</p> <div class="options-section">
<h4>Vacuum database</h4>
<p>This will rebuild the database which will typically result in a smaller database file. No data will be actually changed.</p>
<button id="vacuum-database-button" class="btn">Vacuum database</button>
</div>`;
<button id="vacuum-database-button" class="btn">Vacuum database</button>`; export default class AdvancedOptions extends OptionsTab {
get tabTitle() { return "Advanced" }
export default class AdvancedOptions { lazyRender() {
constructor() { this.$widget = $(TPL);
$("#options-advanced").html(TPL);
this.$forceFullSyncButton = $("#force-full-sync-button"); this.$forceFullSyncButton = this.$widget.find("#force-full-sync-button");
this.$fillEntityChangesButton = $("#fill-entity-changes-button"); this.$fillEntityChangesButton = this.$widget.find("#fill-entity-changes-button");
this.$anonymizeFullButton = $("#anonymize-full-button"); this.$anonymizeFullButton = this.$widget.find("#anonymize-full-button");
this.$anonymizeLightButton = $("#anonymize-light-button"); this.$anonymizeLightButton = this.$widget.find("#anonymize-light-button");
this.$vacuumDatabaseButton = $("#vacuum-database-button"); this.$vacuumDatabaseButton = this.$widget.find("#vacuum-database-button");
this.$findAndFixConsistencyIssuesButton = $("#find-and-fix-consistency-issues-button"); this.$findAndFixConsistencyIssuesButton = this.$widget.find("#find-and-fix-consistency-issues-button");
this.$checkIntegrityButton = $("#check-integrity-button"); this.$checkIntegrityButton = this.$widget.find("#check-integrity-button");
this.$forceFullSyncButton.on('click', async () => { this.$forceFullSyncButton.on('click', async () => {
await server.post('sync/force-full-sync'); await server.post('sync/force-full-sync');

View File

@ -1,6 +1,7 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import utils from "../../../services/utils.js"; import utils from "../../../services/utils.js";
import appContext from "../../../services/app_context.js"; import appContext from "../../../services/app_context.js";
import OptionsTab from "./options_tab.js";
const FONT_FAMILIES = [ const FONT_FAMILIES = [
{ value: "theme", label: "Theme defined" }, { value: "theme", label: "Theme defined" },
@ -30,17 +31,25 @@ const FONT_FAMILIES = [
const TPL = ` const TPL = `
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p> <p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
<form> <style>
.options-section .row {
/* rows otherwise overflow horizontally and force a scrollbar */
margin-left: auto;
margin-right: auto;
}
</style>
<div class="options-section">
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<label for="zoom-factor-select">Zoom factor (desktop build only)</label> <label for="zoom-factor-select">Zoom factor (desktop build only)</label>
<input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/> <input type="number" class="form-control" id="zoom-factor-select" min="0.3" max="2.0" step="0.1"/>
</div> </div>
<div class="col-6"> <div class="col-6">
<label for="native-title-bar-select">Native title bar (requires app restart)</label> <label for="native-title-bar-select">Native title bar (requires app restart)</label>
<select class="form-control" id="native-title-bar-select"> <select class="form-control" id="native-title-bar-select">
<option value="show">enabled</option> <option value="show">enabled</option>
<option value="hide">disabled</option> <option value="hide">disabled</option>
@ -49,9 +58,11 @@ const TPL = `
</div> </div>
<p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p> <p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p>
</div>
<div class="options-section">
<h4>Theme</h4> <h4>Theme</h4>
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<label for="theme-select">Theme</label> <label for="theme-select">Theme</label>
@ -63,104 +74,106 @@ const TPL = `
<input type="checkbox" class="form-control" id="override-theme-fonts"> <input type="checkbox" class="form-control" id="override-theme-fonts">
</div> </div>
</div> </div>
<div class="options-section">
<div id="overriden-font-settings"> <div id="overriden-font-settings" class="options-section">
<h4>Fonts</h4> <h4>Fonts</h4>
<h5>Main font</h5>
<div class="form-group row">
<div class="col-6">
<label for="main-font-family">Font family</label>
<select class="form-control" id="main-font-family"></select>
</div>
<div class="col-6">
<label for="main-font-size">Size</label>
<div class="input-group"> <h5>Main font</h5>
<input type="number" class="form-control" id="main-font-size" min="50" max="200" step="10"/>
<div class="input-group-append"> <div class="form-group row">
<span class="input-group-text">%</span> <div class="col-6">
</div> <label for="main-font-family">Font family</label>
<select class="form-control" id="main-font-family"></select>
</div>
<div class="col-6">
<label for="main-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="main-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div> </div>
</div> </div>
</div> </div>
</div>
<h5>Note tree font</h5>
<div class="form-group row">
<div class="col-4">
<label for="tree-font-family">Font family</label>
<select class="form-control" id="tree-font-family"></select>
</div>
<h5>Note tree font</h5> <div class="col-4">
<label for="tree-font-size">Size</label>
<div class="form-group row">
<div class="col-4"> <div class="input-group">
<label for="tree-font-family">Font family</label> <input type="number" class="form-control" id="tree-font-size" min="50" max="200" step="10"/>
<select class="form-control" id="tree-font-family"></select> <div class="input-group-append">
</div> <span class="input-group-text">%</span>
<div class="col-4">
<label for="tree-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="tree-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<h5>Note detail font</h5>
<div class="form-group row">
<div class="col-4">
<label for="detail-font-family">Font family</label>
<select class="form-control" id="detail-font-family"></select>
</div>
<div class="col-4">
<label for="detail-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="detail-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<h5>Monospace (code) font</h5>
<div class="form-group row">
<div class="col-4">
<label for="monospace-font-family">Font family</label>
<select class="form-control" id="monospace-font-family"></select>
</div>
<div class="col-4">
<label for="monospace-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="monospace-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
<p>Not all listed fonts may be available on your system.</p>
</div> </div>
<p> <h5>Note detail font</h5>
To apply font changes, click on
<button class="btn btn-micro reload-frontend-button">reload frontend</button>
</p>
<div class="form-group row">
<div class="col-4">
<label for="detail-font-family">Font family</label>
<select class="form-control" id="detail-font-family"></select>
</div>
<div class="col-4">
<label for="detail-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="detail-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<h5>Monospace (code) font</h5>
<div class="form-group row">
<div class="col-4">
<label for="monospace-font-family">Font family</label>
<select class="form-control" id="monospace-font-family"></select>
</div>
<div class="col-4">
<label for="monospace-font-size">Size</label>
<div class="input-group">
<input type="number" class="form-control" id="monospace-font-size" min="50" max="200" step="10"/>
<div class="input-group-append">
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
<p>Note that tree and detail font sizing is relative to the main font size setting.</p>
<p>Not all listed fonts may be available on your system.</p>
</div>
<p>
To apply font changes, click on
<button class="btn btn-micro reload-frontend-button">reload frontend</button>
</p>
<div class="options-section">
<h4>Content width</h4> <h4>Content width</h4>
<p>Trilium by default limits max content width to improve readability for maximized screens on wide screens.</p> <p>Trilium by default limits max content width to improve readability for maximized screens on wide screens.</p>
<div class="form-group row"> <div class="form-group row">
<div class="col-4"> <div class="col-4">
<label for="max-content-width">Max content width in pixels</label> <label for="max-content-width">Max content width in pixels</label>
@ -172,35 +185,38 @@ const TPL = `
To content width changes, click on To content width changes, click on
<button class="btn btn-micro reload-frontend-button">reload frontend</button> <button class="btn btn-micro reload-frontend-button">reload frontend</button>
</p> </p>
</form>`; </div>
`;
export default class ApperanceOptions { export default class AppearanceOptions extends OptionsTab {
constructor() { get tabTitle() { return "Appearance" }
$("#options-appearance").html(TPL);
this.$zoomFactorSelect = $("#zoom-factor-select"); lazyRender() {
this.$nativeTitleBarSelect = $("#native-title-bar-select"); this.$widget = $(TPL);
this.$themeSelect = $("#theme-select"); this.$zoomFactorSelect = this.$widget.find("#zoom-factor-select");
this.$overrideThemeFonts = $("#override-theme-fonts"); this.$nativeTitleBarSelect = this.$widget.find("#native-title-bar-select");
this.$overridenFontSettings = $("#overriden-font-settings"); this.$themeSelect = this.$widget.find("#theme-select");
this.$overrideThemeFonts = this.$widget.find("#override-theme-fonts");
this.$mainFontSize = $("#main-font-size"); this.$overridenFontSettings = this.$widget.find("#overriden-font-settings");
this.$mainFontFamily = $("#main-font-family");
this.$treeFontSize = $("#tree-font-size"); this.$mainFontSize = this.$widget.find("#main-font-size");
this.$treeFontFamily = $("#tree-font-family"); this.$mainFontFamily = this.$widget.find("#main-font-family");
this.$detailFontSize = $("#detail-font-size"); this.$treeFontSize = this.$widget.find("#tree-font-size");
this.$detailFontFamily = $("#detail-font-family"); this.$treeFontFamily = this.$widget.find("#tree-font-family");
this.$monospaceFontSize = $("#monospace-font-size"); this.$detailFontSize = this.$widget.find("#detail-font-size");
this.$monospaceFontFamily = $("#monospace-font-family"); this.$detailFontFamily = this.$widget.find("#detail-font-family");
$(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options")); this.$monospaceFontSize = this.$widget.find("#monospace-font-size");
this.$monospaceFontFamily = this.$widget.find("#monospace-font-family");
this.$body = $("body"); this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
this.$body = this.$widget.find("body");
this.$themeSelect.on('change', async () => { this.$themeSelect.on('change', async () => {
const newTheme = this.$themeSelect.val(); const newTheme = this.$themeSelect.val();
@ -211,11 +227,9 @@ export default class ApperanceOptions {
}); });
this.$overrideThemeFonts.on('change', async () => { this.$overrideThemeFonts.on('change', async () => {
const isOverriden = this.$overrideThemeFonts.is(":checked"); this.updateCheckboxOption('overrideThemeFonts', this.$overrideThemeFonts);
await server.put('options/overrideThemeFonts/' + isOverriden.toString()); this.$overridenFontSettings.toggle(this.$overrideThemeFonts.is(":checked"));
this.$overridenFontSettings.toggle(isOverriden);
}); });
this.$zoomFactorSelect.on('change', () => { appContext.triggerCommand('setZoomFactorAndSave', {zoomFactor: this.$zoomFactorSelect.val()}); }); this.$zoomFactorSelect.on('change', () => { appContext.triggerCommand('setZoomFactorAndSave', {zoomFactor: this.$zoomFactorSelect.val()}); });
@ -223,7 +237,7 @@ export default class ApperanceOptions {
this.$nativeTitleBarSelect.on('change', () => { this.$nativeTitleBarSelect.on('change', () => {
const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false'; const nativeTitleBarVisible = this.$nativeTitleBarSelect.val() === 'show' ? 'true' : 'false';
server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible); this.updateOption('nativeTitleBarVisible', nativeTitleBarVisible);
}); });
const optionsToSave = [ const optionsToSave = [
@ -234,16 +248,14 @@ export default class ApperanceOptions {
]; ];
for (const optionName of optionsToSave) { for (const optionName of optionsToSave) {
this['$' + optionName].on('change', () => server.put(`options/${optionName}/${this['$' + optionName].val()}`)); this['$' + optionName].on('change', () =>
this.updateOption(optionName, this['$' + optionName].val()));
} }
this.$maxContentWidth = $("#max-content-width"); this.$maxContentWidth = this.$widget.find("#max-content-width");
this.$maxContentWidth.on('change', async () => { this.$maxContentWidth.on('change', async () =>
const maxContentWidth = this.$maxContentWidth.val(); this.updateOption('maxContentWidth', this.$maxContentWidth.val()))
await server.put('options/maxContentWidth/' + maxContentWidth);
})
} }
toggleBodyClass(prefix, value) { toggleBodyClass(prefix, value) {
@ -282,7 +294,7 @@ export default class ApperanceOptions {
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);
this.$overrideThemeFonts.prop('checked', options.overrideThemeFonts === 'true'); this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
this.$overridenFontSettings.toggle(options.overrideThemeFonts === 'true'); this.$overridenFontSettings.toggle(options.overrideThemeFonts === 'true');
this.$mainFontSize.val(options.mainFontSize); this.$mainFontSize.val(options.mainFontSize);

View File

@ -1,42 +1,47 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4>Automatic backup</h4> <div class="options-section">
<h4>Automatic backup</h4>
<p>Trilium can back up the database automatically:</p>
<p>Trilium can back up the database automatically:</p>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="daily-backup-enabled"> <div class="custom-control custom-checkbox">
<label class="custom-control-label" for="daily-backup-enabled">Enable daily backup</label> <input type="checkbox" class="custom-control-input" id="daily-backup-enabled">
<label class="custom-control-label" for="daily-backup-enabled">Enable daily backup</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="weekly-backup-enabled">
<label class="custom-control-label" for="weekly-backup-enabled">Enable weekly backup</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="monthly-backup-enabled">
<label class="custom-control-label" for="monthly-backup-enabled">Enable monthly backup</label>
</div>
<br/>
<p>It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.</p>
</div> </div>
<div class="custom-control custom-checkbox"> <div class="options-section">
<input type="checkbox" class="custom-control-input" id="weekly-backup-enabled"> <h4>Backup now</h4>
<label class="custom-control-label" for="weekly-backup-enabled">Enable weekly backup</label>
<button id="backup-database-button" class="btn">Backup database now</button>
</div> </div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="monthly-backup-enabled">
<label class="custom-control-label" for="monthly-backup-enabled">Enable monthly backup</label>
</div>
<br/>
<p>It's recommended to keep the backup turned on, but this can make application startup slow with large databases and/or slow storage devices.</p>
<br/>
<h4>Backup now</h4>
<button id="backup-database-button" class="btn">Backup database now</button><br/><br/>
`; `;
export default class BackupOptions { export default class BackupOptions extends OptionsTab {
constructor() { get tabTitle() { return "Backup" }
$("#options-backup").html(TPL);
this.$backupDatabaseButton = $("#backup-database-button"); lazyRender() {
this.$widget = $(TPL);
this.$backupDatabaseButton = this.$widget.find("#backup-database-button");
this.$backupDatabaseButton.on('click', async () => { this.$backupDatabaseButton.on('click', async () => {
const {backupFile} = await server.post('database/backup-database'); const {backupFile} = await server.post('database/backup-database');
@ -44,35 +49,23 @@ export default class BackupOptions {
toastService.showMessage("Database has been backed up to " + backupFile, 10000); toastService.showMessage("Database has been backed up to " + backupFile, 10000);
}); });
this.$dailyBackupEnabled = $("#daily-backup-enabled"); this.$dailyBackupEnabled = this.$widget.find("#daily-backup-enabled");
this.$weeklyBackupEnabled = $("#weekly-backup-enabled"); this.$weeklyBackupEnabled = this.$widget.find("#weekly-backup-enabled");
this.$monthlyBackupEnabled = $("#monthly-backup-enabled"); this.$monthlyBackupEnabled = this.$widget.find("#monthly-backup-enabled");
this.$dailyBackupEnabled.on('change', () => { this.$dailyBackupEnabled.on('change', () =>
const opts = { 'dailyBackupEnabled': this.$dailyBackupEnabled.is(":checked") ? "true" : "false" }; this.updateCheckboxOption('dailyBackupEnabled', this.$dailyBackupEnabled));
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false; this.$weeklyBackupEnabled.on('change', () =>
}); this.updateCheckboxOption('weeklyBackupEnabled', this.$weeklyBackupEnabled));
this.$weeklyBackupEnabled.on('change', () => { this.$monthlyBackupEnabled.on('change', () =>
const opts = { 'weeklyBackupEnabled': this.$weeklyBackupEnabled.is(":checked") ? "true" : "false" }; this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$monthlyBackupEnabled.on('change', () => {
const opts = { 'monthlyBackupEnabled': this.$monthlyBackupEnabled.is(":checked") ? "true" : "false" };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
} }
optionsLoaded(options) { optionsLoaded(options) {
this.$dailyBackupEnabled.prop("checked", options['dailyBackupEnabled'] === 'true'); this.setCheckboxState(this.$dailyBackupEnabled, options.dailyBackupEnabled);
this.$weeklyBackupEnabled.prop("checked", options['weeklyBackupEnabled'] === 'true'); this.setCheckboxState(this.$weeklyBackupEnabled, options.weeklyBackupEnabled);
this.$monthlyBackupEnabled.prop("checked", options['monthlyBackupEnabled'] === 'true'); this.setCheckboxState(this.$monthlyBackupEnabled, options.monthlyBackupEnabled);
} }
} }

View File

@ -2,34 +2,69 @@ import mimeTypesService from "../../../services/mime_types.js";
import options from "../../../services/options.js"; import options from "../../../services/options.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import utils from "../../../services/utils.js"; import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4> <div class="options-section">
<div class="custom-control custom-checkbox"> <h4>Use vim keybindings in code notes (no ex mode)</h4>
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled"> <div class="custom-control custom-checkbox">
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label> <input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
</div>
</div> </div>
<h4>Available MIME types in the dropdown</h4>
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`; <div class="options-section">
<h4>Wrap lines in code notes</h4>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="line-wrap-enabled">
<label class="custom-control-label" for="line-wrap-enabled">Enable Line Wrap (change might need a frontend reload to take effect)</label>
</div>
</div>
export default class CodeNotesOptions { <div class="options-section">
constructor() { <h4>Automatic readonly size</h4>
$("#options-code-notes").html(TPL);
this.$vimKeymapEnabled = $("#vim-keymap-enabled"); <p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
this.$vimKeymapEnabled.on('change', () => {
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" }; <div class="form-group">
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved.")); <label for="auto-readonly-size-code">Automatic readonly size (code notes)</label>
return false; <input class="form-control" id="auto-readonly-size-code" type="number" min="0">
}); </div>
this.$mimeTypes = $("#options-mime-types"); </div>
<div class="options-section">
<h4>Available MIME types in the dropdown</h4>
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>
</div>`;
export default class CodeNotesOptions extends OptionsTab {
get tabTitle() { return "Code notes" }
lazyRender() {
this.$widget = $(TPL);
this.$vimKeymapEnabled = this.$widget.find("#vim-keymap-enabled");
this.$vimKeymapEnabled.on('change', () =>
this.updateCheckboxOption('vimKeymapEnabled', this.$vimKeymapEnabled));
this.$codeLineWrapEnabled = this.$widget.find("#line-wrap-enabled");
this.$codeLineWrapEnabled.on('change', () =>
this.updateCheckboxOption('codeLineWrapEnabled', this.$codeLineWrapEnabled));
this.$mimeTypes = this.$widget.find("#options-mime-types");
this.$autoReadonlySizeCode = this.$widget.find("#auto-readonly-size-code");
this.$autoReadonlySizeCode.on('change', () =>
this.updateOption('autoReadonlySizeCode', this.$autoReadonlySizeCode.val()));
} }
async optionsLoaded(options) { async optionsLoaded(options) {
this.$mimeTypes.empty(); this.$mimeTypes.empty();
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true'); this.setCheckboxState(this.$vimKeymapEnabled, options.vimKeymapEnabled);
this.setCheckboxState(this.$codeLineWrapEnabled, options.codeLineWrapEnabled);
this.$autoReadonlySizeCode.val(options.autoReadonlySizeCode);
let idCtr = 1; let idCtr = 1;
for (const mimeType of await mimeTypesService.getMimeTypes()) { for (const mimeType of await mimeTypesService.getMimeTypes()) {
@ -53,9 +88,9 @@ export default class CodeNotesOptions {
const enabledMimeTypes = []; const enabledMimeTypes = [];
this.$mimeTypes.find("input:checked").each( this.$mimeTypes.find("input:checked").each(
(i, el) => enabledMimeTypes.push($(el).attr("data-mime-type"))); (i, el) => enabledMimeTypes.push(this.$widget.find(el).attr("data-mime-type")));
await options.save('codeNotesMimeTypes', JSON.stringify(enabledMimeTypes)); await this.updateOption('codeNotesMimeTypes', JSON.stringify(enabledMimeTypes));
mimeTypesService.loadMimeTypes(); mimeTypesService.loadMimeTypes();
} }

View File

@ -1,32 +1,33 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import dialogService from "../../dialog.js"; import dialogService from "../../dialog.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4>ETAPI</h4> <div class="options-section">
<h4>ETAPI</h4>
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/>
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
<p>ETAPI is a REST API used to access Trilium instance programmatically, without UI. <br/> <h5>Existing tokens</h5>
See more details on <a href="https://github.com/zadam/trilium/wiki/ETAPI">wiki</a> and <a onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">ETAPI OpenAPI spec</a>.</p>
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div>
<button type="button" class="btn btn-sm" id="create-etapi-token">Create new ETAPI token</button>
<div style="overflow: auto; height: 500px;">
<br/><br/> <table id="tokens-table" class="table table-stripped">
<thead>
<h5>Existing tokens</h5> <tr>
<th>Token name</th>
<div id="no-tokens-yet">There are no tokens yet. Click on the button above to create one.</div> <th>Created</th>
<th>Actions</th>
<div style="overflow: auto; height: 500px;"> </tr>
<table id="tokens-table" class="table table-stripped"> </thead>
<thead> <tbody></tbody>
<tr> </table>
<th>Token name</th> </div>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div> </div>
<style> <style>
@ -45,11 +46,13 @@ const TPL = `
} }
</style>`; </style>`;
export default class EtapiOptions { export default class EtapiOptions extends OptionsTab {
constructor() { get tabTitle() { return "ETAPI" }
$("#options-etapi").html(TPL);
$("#create-etapi-token").on("click", async () => { lazyRender() {
this.$widget = $(TPL);
this.$widget.find("#create-etapi-token").on("click", async () => {
const tokenName = await dialogService.prompt({ const tokenName = await dialogService.prompt({
title: "New ETAPI token", title: "New ETAPI token",
message: "Please enter new token's name", message: "Please enter new token's name",
@ -76,8 +79,8 @@ export default class EtapiOptions {
} }
async refreshTokens() { async refreshTokens() {
const $noTokensYet = $("#no-tokens-yet"); const $noTokensYet = this.$widget.find("#no-tokens-yet");
const $tokensTable = $("#tokens-table"); const $tokensTable = this.$widget.find("#tokens-table");
const tokens = await server.get('etapi-tokens'); const tokens = await server.get('etapi-tokens');

View File

@ -0,0 +1,85 @@
import OptionsTab from "./options_tab.js";
const TPL = `
<style>
.options-section .disabled-field {
opacity: 0.5;
pointer-events: none;
}
</style>
<div class="options-section">
<h4>Images</h4>
<div class="form-group">
<input id="download-images-automatically" type="checkbox" name="download-images-automatically">
<label for="download-images-automatically">Download images automatically for offline use.</label>
<p>(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)</p>
</div>
<div class="form-group">
<input id="image-compresion-enabled" type="checkbox" name="image-compression-enabled">
<label for="image-compresion-enabled">Enable image compression</label>
</div>
<div id="image-compression-enabled-wraper">
<div class="form-group">
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
<input class="form-control" id="image-max-width-height" type="number" min="1">
</div>
<div class="form-group">
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
</div>
</div>
</div>
`;
export default class ImageOptions extends OptionsTab {
get tabTitle() { return "Images" }
lazyRender() {
this.$widget = $(TPL);
this.$imageMaxWidthHeight = this.$widget.find("#image-max-width-height");
this.$imageJpegQuality = this.$widget.find("#image-jpeg-quality");
this.$imageMaxWidthHeight.on('change', () =>
this.updateOption('imageMaxWidthHeight', this.$imageMaxWidthHeight.val()));
this.$imageJpegQuality.on('change', () =>
this.updateOption('imageJpegQuality', this.$imageJpegQuality.val()));
this.$downloadImagesAutomatically = this.$widget.find("#download-images-automatically");
this.$downloadImagesAutomatically.on("change", () =>
this.updateCheckboxOption('downloadImagesAutomatically', this.$downloadImagesAutomatically));
this.$enableImageCompression = this.$widget.find("#image-compresion-enabled");
this.$imageCompressionWrapper = this.$widget.find("#image-compression-enabled-wraper");
this.$enableImageCompression.on("change", () => {
this.updateCheckboxOption('compressImages', this.$enableImageCompression);
this.setImageCompression();
});
}
optionsLoaded(options) {
this.$imageMaxWidthHeight.val(options.imageMaxWidthHeight);
this.$imageJpegQuality.val(options.imageJpegQuality);
this.setCheckboxState(this.$downloadImagesAutomatically, options.downloadImagesAutomatically);
this.setCheckboxState(this.$enableImageCompression, options.compressImages);
this.setImageCompression();
}
setImageCompression() {
if (this.$enableImageCompression.prop("checked")) {
this.$imageCompressionWrapper.removeClass("disabled-field");
} else {
this.$imageCompressionWrapper.addClass("disabled-field");
}
}
}

View File

@ -0,0 +1,37 @@
import BasicWidget from "../../basic_widget.js";
import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
export default class OptionsTab extends BasicWidget {
async updateOption(name, value) {
const opts = { [name]: value };
await this.updateMultipleOptions(opts);
}
async updateMultipleOptions(opts) {
await server.put('options', opts);
this.showUpdateNotification();
}
showUpdateNotification() {
toastService.showPersistent({
id: "options-change-saved",
title: "Options status",
message: "Options change have been saved.",
icon: "slider",
closeAfter: 2000
});
}
async updateCheckboxOption(name, $checkbox) {
const isChecked = $checkbox.prop("checked");
return await this.updateOption(name, isChecked ? 'true' : 'false');
}
setCheckboxState($checkbox, optionValue) {
$checkbox.prop('checked', optionValue === 'true');
}
}

View File

@ -1,65 +1,18 @@
import utils from "../../../services/utils.js";
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<style> <div class="options-section">
.disabled-field { <h4>Tray</h4>
opacity: 0.5;
pointer-events: none;
}
</style>
<div>
<h4>Spell check</h4>
<p>These options apply only for desktop builds, browsers will use their own native spell check. App restart is required after change.</p>
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="spell-check-enabled"> <input type="checkbox" class="custom-control-input" id="tray-enabled">
<label class="custom-control-label" for="spell-check-enabled">Enable spellcheck</label> <label class="custom-control-label" for="tray-enabled">Enable tray (Trilium needs to be restarted for this change to take effect)</label>
</div>
<br/>
<div class="form-group">
<label for="spell-check-language-code">Language code(s)</label>
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
</div>
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p>
<p><strong>Available language codes: </strong> <span id="available-language-codes"></span></p>
</div>
<div>
<h4>Images</h4>
<div class="form-group">
<input id="download-images-automatically" type="checkbox" name="download-images-automatically">
<label for="download-images-automatically">Download images automatically for offline use.</label>
<p>(pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline)</p>
</div>
<div class="form-group">
<input id="image-compresion-enabled" type="checkbox" name="image-compression-enabled">
<label for="image-compresion-enabled">Enable image compression</label>
</div>
<div id="image-compression-enabled-wraper">
<div class="form-group">
<label for="image-max-width-height">Max width / height of an image in pixels (image will be resized if it exceeds this setting).</label>
<input class="form-control" id="image-max-width-height" type="number" min="1">
</div>
<div class="form-group">
<label for="image-jpeg-quality">JPEG quality (10 - worst quality, 100 best quality, 50 - 85 is recommended)</label>
<input class="form-control" id="image-jpeg-quality" min="10" max="100" type="number">
</div>
</div> </div>
</div> </div>
<div> <div class="options-section">
<h4>Note erasure timeout</h4> <h4>Note erasure timeout</h4>
<p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them <p>Deleted notes (and attributes, revisions...) are at first only marked as deleted and it is possible to recover them
@ -75,23 +28,9 @@ const TPL = `
<p>You can also trigger erasing manually:</p> <p>You can also trigger erasing manually:</p>
<button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button> <button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button>
<br/><br/>
</div> </div>
<div> <div class="options-section">
<h4>Protected session timeout</h4>
<p>Protected session timeout is a time period after which the protected session is wiped from
the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
<div class="form-group">
<label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
<input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
</div>
</div>
<div>
<h4>Note revisions snapshot interval</h4> <h4>Note revisions snapshot interval</h4>
<p>Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href="https://github.com/zadam/trilium/wiki/Note-revisions" class="external">wiki</a> for more info.</p> <p>Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href="https://github.com/zadam/trilium/wiki/Note-revisions" class="external">wiki</a> for more info.</p>
@ -102,198 +41,51 @@ const TPL = `
</div> </div>
</div> </div>
<div> <div class="options-section">
<h4>Automatic readonly size</h4> <h4>Network connections</h4>
<p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
<div class="form-group"> <div class="form-group">
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label> <input id="check-for-updates" type="checkbox" name="check-for-updates">
<input class="form-control" id="auto-readonly-size-text" type="number" min="0"> <label for="check-for-updates">Check for updates automatically</label>
</div> </div>
</div>`;
<div class="form-group"> export default class OtherOptions extends OptionsTab {
<label for="auto-readonly-size-code">Automatic readonly size (code notes)</label> get tabTitle() { return "Other" }
<input class="form-control" id="auto-readonly-size-code" type="number" min="0">
</div>
</div>
<div>
<h4>Network connections</h4>
<div class="form-group">
<input id="check-for-updates" type="checkbox" name="check-for-updates">
<label for="check-for-updates">Check for updates automatically</label>
</div>
</div>
`; lazyRender() {
this.$widget = $(TPL);
export default class ProtectedSessionOptions { this.$trayEnabled = this.$widget.find("#tray-enabled");
constructor() { this.$trayEnabled.on('change', () =>
$("#options-other").html(TPL); this.updateOption('disableTray', !this.$trayEnabled.is(":checked") ? "true" : "false"));
this.$spellCheckEnabled = $("#spell-check-enabled"); this.$eraseEntitiesAfterTimeInSeconds = this.$widget.find("#erase-entities-after-time-in-seconds");
this.$spellCheckLanguageCode = $("#spell-check-language-code"); this.$eraseEntitiesAfterTimeInSeconds.on('change', () => this.updateOption('eraseEntitiesAfterTimeInSeconds', this.$eraseEntitiesAfterTimeInSeconds.val()));
this.$spellCheckEnabled.on('change', () => { this.$eraseDeletedNotesButton = this.$widget.find("#erase-deleted-notes-now-button");
const opts = { 'spellCheckEnabled': this.$spellCheckEnabled.is(":checked") ? "true" : "false" };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$spellCheckLanguageCode.on('change', () => {
const opts = { 'spellCheckLanguageCode': this.$spellCheckLanguageCode.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$availableLanguageCodes = $("#available-language-codes");
if (utils.isElectron()) {
const { webContents } = utils.dynamicRequire('@electron/remote').getCurrentWindow();
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
}
this.$eraseEntitiesAfterTimeInSeconds = $("#erase-entities-after-time-in-seconds");
this.$eraseEntitiesAfterTimeInSeconds.on('change', () => {
const eraseEntitiesAfterTimeInSeconds = this.$eraseEntitiesAfterTimeInSeconds.val();
server.put('options', { 'eraseEntitiesAfterTimeInSeconds': eraseEntitiesAfterTimeInSeconds }).then(() => {
toastService.showMessage("Options changed have been saved.");
});
return false;
});
this.$eraseDeletedNotesButton = $("#erase-deleted-notes-now-button");
this.$eraseDeletedNotesButton.on('click', () => { this.$eraseDeletedNotesButton.on('click', () => {
server.post('notes/erase-deleted-notes-now').then(() => { server.post('notes/erase-deleted-notes-now').then(() => {
toastService.showMessage("Deleted notes have been erased."); toastService.showMessage("Deleted notes have been erased.");
}); });
}); });
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); this.$noteRevisionsTimeInterval = this.$widget.find("#note-revision-snapshot-time-interval-in-seconds");
this.$protectedSessionTimeout.on('change', () => { this.$noteRevisionsTimeInterval.on('change', () =>
const protectedSessionTimeout = this.$protectedSessionTimeout.val(); this.updateOption('noteRevisionSnapshotTimeInterval', this.$noteRevisionsTimeInterval.val()));
server.put('options', { 'protectedSessionTimeout': protectedSessionTimeout }).then(() => { this.$checkForUpdates = this.$widget.find("#check-for-updates");
toastService.showMessage("Options changed have been saved."); this.$checkForUpdates.on("change", () =>
}); this.updateCheckboxOption('checkForUpdates', this.$checkForUpdates));
return false;
});
this.$noteRevisionsTimeInterval = $("#note-revision-snapshot-time-interval-in-seconds");
this.$noteRevisionsTimeInterval.on('change', () => {
const opts = { 'noteRevisionSnapshotTimeInterval': this.$noteRevisionsTimeInterval.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$imageMaxWidthHeight = $("#image-max-width-height");
this.$imageJpegQuality = $("#image-jpeg-quality");
this.$imageMaxWidthHeight.on('change', () => {
const opts = { 'imageMaxWidthHeight': this.$imageMaxWidthHeight.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$imageJpegQuality.on('change', () => {
const opts = { 'imageJpegQuality': this.$imageJpegQuality.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$autoReadonlySizeText = $("#auto-readonly-size-text");
this.$autoReadonlySizeText.on('change', () => {
const opts = { 'autoReadonlySizeText': this.$autoReadonlySizeText.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$autoReadonlySizeCode = $("#auto-readonly-size-code");
this.$autoReadonlySizeCode.on('change', () => {
const opts = { 'autoReadonlySizeCode': this.$autoReadonlySizeText.val() };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false;
});
this.$downloadImagesAutomatically = $("#download-images-automatically");
this.$downloadImagesAutomatically.on("change", () => {
const isChecked = this.$downloadImagesAutomatically.prop("checked");
const opts = { 'downloadImagesAutomatically': isChecked ? 'true' : 'false' };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
});
this.$enableImageCompression = $("#image-compresion-enabled");
this.$imageCompressionWrapper = $("#image-compression-enabled-wraper");
this.setImageCompression = (isChecked) => {
if (isChecked) {
this.$imageCompressionWrapper.removeClass("disabled-field");
} else {
this.$imageCompressionWrapper.addClass("disabled-field");
}
};
this.$enableImageCompression.on("change", () => {
const isChecked = this.$enableImageCompression.prop("checked");
const opts = { 'compressImages': isChecked ? 'true' : 'false' };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
this.setImageCompression(isChecked);
});
this.$checkForUpdates = $("#check-for-updates");
this.$checkForUpdates.on("change", () => {
const isChecked = this.$checkForUpdates.prop("checked");
const opts = { 'checkForUpdates': isChecked ? 'true' : 'false' };
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
});
} }
optionsLoaded(options) { optionsLoaded(options) {
this.$spellCheckEnabled.prop("checked", options['spellCheckEnabled'] === 'true'); this.$trayEnabled.prop("checked", options.disableTray !== 'true');
this.$spellCheckLanguageCode.val(options['spellCheckLanguageCode']);
this.$eraseEntitiesAfterTimeInSeconds.val(options['eraseEntitiesAfterTimeInSeconds']); this.$eraseEntitiesAfterTimeInSeconds.val(options.eraseEntitiesAfterTimeInSeconds);
this.$protectedSessionTimeout.val(options['protectedSessionTimeout']); this.$noteRevisionsTimeInterval.val(options.noteRevisionSnapshotTimeInterval);
this.$noteRevisionsTimeInterval.val(options['noteRevisionSnapshotTimeInterval']);
this.$imageMaxWidthHeight.val(options['imageMaxWidthHeight']);
this.$imageJpegQuality.val(options['imageJpegQuality']);
this.$autoReadonlySizeText.val(options['autoReadonlySizeText']);
this.$autoReadonlySizeCode.val(options['autoReadonlySizeCode']);
const downloadImagesAutomatically = options['downloadImagesAutomatically'] === 'true';
this.$downloadImagesAutomatically.prop('checked', downloadImagesAutomatically);
const compressImages = options['compressImages'] === 'true';
this.$enableImageCompression.prop('checked', compressImages);
this.setImageCompression(compressImages);
const checkForUpdates = options['checkForUpdates'] === 'true';
this.$checkForUpdates.prop('checked', checkForUpdates);
this.setCheckboxState(this.$checkForUpdates, options.checkForUpdates);
} }
} }

View File

@ -1,46 +1,63 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import protectedSessionHolder from "../../../services/protected_session_holder.js"; import protectedSessionHolder from "../../../services/protected_session_holder.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h3 id="password-heading"></h3> <div class="options-section">
<h4 id="password-heading"></h4>
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
Please take care to remember your new password. Password is used to encrypt protected notes. <div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
If you forget your password, then all your protected notes are forever lost. Please take care to remember your new password. Password is used for logging into the web interface and
In case you did forget your password, <a id="reset-password-button" href="javascript:">click here to reset it</a>. to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.
In case you did forget your password, <a id="reset-password-button" href="javascript:">click here to reset it</a>.
</div>
<form id="change-password-form">
<div class="form-group" id="old-password-form-group">
<label for="old-password">Old password</label>
<input class="form-control" id="old-password" type="password">
</div>
<div class="form-group">
<label for="new-password1">New password</label>
<input class="form-control" id="new-password1" type="password">
</div>
<div class="form-group">
<label for="new-password2">New password Confirmation</label>
<input class="form-control" id="new-password2" type="password">
</div>
<button class="btn btn-primary" id="save-password-button">Change password</button>
</form>
</div> </div>
<form id="change-password-form"> <div class="options-section">
<div class="form-group" id="old-password-form-group"> <h4>Protected session timeout</h4>
<label for="old-password">Old password</label>
<input class="form-control" id="old-password" type="password"> <p>Protected session timeout is a time period after which the protected session is wiped from
</div> the browser's memory. This is measured from the last interaction with protected notes. See <a href="https://github.com/zadam/trilium/wiki/Protected-notes" class="external">wiki</a> for more info.</p>
<div class="form-group"> <div class="form-group">
<label for="new-password1">New password</label> <label for="protected-session-timeout-in-seconds">Protected session timeout (in seconds)</label>
<input class="form-control" id="new-password1" type="password"> <input class="form-control" id="protected-session-timeout-in-seconds" type="number" min="60">
</div> </div>
</div>`;
<div class="form-group"> export default class PasswordOptions extends OptionsTab {
<label for="new-password2">New password Confirmation</label> get tabTitle() { return "Password" }
<input class="form-control" id="new-password2" type="password">
</div>
<button class="btn btn-primary" id="save-password-button">Change password</button> lazyRender() {
</form>`; this.$widget = $(TPL);
export default class ChangePasswordOptions { this.$passwordHeading = this.$widget.find("#password-heading");
constructor() { this.$changePasswordForm = this.$widget.find("#change-password-form");
$("#options-password").html(TPL); this.$oldPassword = this.$widget.find("#old-password");
this.$newPassword1 = this.$widget.find("#new-password1");
this.$passwordHeading = $("#password-heading"); this.$newPassword2 = this.$widget.find("#new-password2");
this.$form = $("#change-password-form"); this.$savePasswordButton = this.$widget.find("#save-password-button");
this.$oldPassword = $("#old-password"); this.$resetPasswordButton = this.$widget.find("#reset-password-button");
this.$newPassword1 = $("#new-password1");
this.$newPassword2 = $("#new-password2");
this.$savePasswordButton = $("#save-password-button");
this.$resetPasswordButton = $("#reset-password-button");
this.$resetPasswordButton.on("click", async () => { this.$resetPasswordButton.on("click", async () => {
if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) { if (confirm("By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?")) {
@ -53,15 +70,20 @@ export default class ChangePasswordOptions {
} }
}); });
this.$form.on('submit', () => this.save()); this.$changePasswordForm.on('submit', () => this.save());
this.$protectedSessionTimeout = this.$widget.find("#protected-session-timeout-in-seconds");
this.$protectedSessionTimeout.on('change', () =>
this.updateOption('protectedSessionTimeout', this.$protectedSessionTimeout.val()));
} }
optionsLoaded(options) { optionsLoaded(options) {
const isPasswordSet = options.isPasswordSet === 'true'; const isPasswordSet = options.isPasswordSet === 'true';
$("#old-password-form-group").toggle(isPasswordSet); this.$widget.find("#old-password-form-group").toggle(isPasswordSet);
this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password'); this.$passwordHeading.text(isPasswordSet ? 'Change password' : 'Set password');
this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password'); this.$savePasswordButton.text(isPasswordSet ? 'Change password' : 'Set password');
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
} }
save() { save() {

View File

@ -1,46 +1,50 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import utils from "../../../services/utils.js"; import utils from "../../../services/utils.js";
import dialogService from "../../dialog.js"; import dialogService from "../../dialog.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4>Keyboard shortcuts</h4> <div class="options-section">
<h4>Keyboard shortcuts</h4>
<p>Multiple shortcuts for the same action can be separated by comma.</p>
<div class="form-group">
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
</div>
<div style="overflow: auto; height: 500px;">
<table id="keyboard-shortcut-table" cellpadding="10">
<thead>
<tr>
<th>Action name</th>
<th>Shortcuts</th>
<th>Default shortcuts</th>
<th>Description</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div style="display: flex; justify-content: space-between">
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button> <p>Multiple shortcuts for the same action can be separated by comma.</p>
</div>
`; <div class="form-group">
<input type="text" class="form-control" id="keyboard-shortcut-filter" placeholder="Type text to filter shortcuts...">
</div>
<div style="overflow: auto; height: 500px;">
<table id="keyboard-shortcut-table" cellpadding="10">
<thead>
<tr>
<th>Action name</th>
<th>Shortcuts</th>
<th>Default shortcuts</th>
<th>Description</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div style="display: flex; justify-content: space-between">
<button class="btn btn-primary" id="options-keyboard-shortcuts-reload-app">Reload app to apply changes</button>
<button class="btn" id="options-keyboard-shortcuts-set-all-to-default">Set all shortcuts to the default</button>
</div>
</div>`;
let globActions; let globActions;
export default class KeyboardShortcutsOptions { export default class KeyboardShortcutsOptions extends OptionsTab {
constructor() { get tabTitle() { return "Shortcuts" }
$("#options-shortcuts").html(TPL);
$("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp()); lazyRender() {
this.$widget = $(TPL);
const $table = $("#keyboard-shortcut-table tbody"); this.$widget.find("#options-keyboard-shortcuts-reload-app").on("click", () => utils.reloadFrontendApp());
const $table = this.$widget.find("#keyboard-shortcut-table tbody");
server.get('keyboard-actions').then(actions => { server.get('keyboard-actions').then(actions => {
globActions = actions; globActions = actions;
@ -73,7 +77,7 @@ export default class KeyboardShortcutsOptions {
}); });
$table.on('change', 'input.form-control', e => { $table.on('change', 'input.form-control', e => {
const $input = $(e.target); const $input = this.$widget.find(e.target);
const actionName = $input.attr('data-keyboard-action-name'); const actionName = $input.attr('data-keyboard-action-name');
const shortcuts = $input.val() const shortcuts = $input.val()
.replace('+,', "+Comma") .replace('+,', "+Comma")
@ -81,54 +85,53 @@ export default class KeyboardShortcutsOptions {
.map(shortcut => shortcut.replace("+Comma", "+,")) .map(shortcut => shortcut.replace("+Comma", "+,"))
.filter(shortcut => !!shortcut); .filter(shortcut => !!shortcut);
const opts = {}; const optionName = 'keyboardShortcuts' + actionName.substr(0, 1).toUpperCase() + actionName.substr(1);
opts['keyboardShortcuts' + actionName.substr(0, 1).toUpperCase() + actionName.substr(1)] = JSON.stringify(shortcuts);
server.put('options', opts); this.updateOption(optionName, JSON.stringify(shortcuts));
}); });
$("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => { this.$widget.find("#options-keyboard-shortcuts-set-all-to-default").on('click', async () => {
if (!await dialogService.confirm("Do you really want to reset all keyboard shortcuts to the default?")) { if (!await dialogService.confirm("Do you really want to reset all keyboard shortcuts to the default?")) {
return; return;
} }
$table.find('input.form-control').each(function() { $table.find('input.form-control').each(function() {
const defaultShortcuts = $(this).attr('data-default-keyboard-shortcuts'); const defaultShortcuts = this.$widget.find(this).attr('data-default-keyboard-shortcuts');
if ($(this).val() !== defaultShortcuts) { if (this.$widget.find(this).val() !== defaultShortcuts) {
$(this) this.$widget.find(this)
.val(defaultShortcuts) .val(defaultShortcuts)
.trigger('change'); .trigger('change');
} }
}); });
}); });
const $filter = $("#keyboard-shortcut-filter"); const $filter = this.$widget.find("#keyboard-shortcut-filter");
$filter.on('keyup', () => { $filter.on('keyup', () => {
const filter = $filter.val().trim().toLowerCase(); const filter = $filter.val().trim().toLowerCase();
$table.find("tr").each((i, el) => { $table.find("tr").each((i, el) => {
if (!filter) { if (!filter) {
$(el).show(); this.$widget.find(el).show();
return; return;
} }
const actionName = $(el).find('input').attr('data-keyboard-action-name'); const actionName = this.$widget.find(el).find('input').attr('data-keyboard-action-name');
if (!actionName) { if (!actionName) {
$(el).hide(); this.$widget.find(el).hide();
return; return;
} }
const action = globActions.find(act => act.actionName === actionName); const action = globActions.find(act => act.actionName === actionName);
if (!action) { if (!action) {
$(el).hide(); this.$widget.find(el).hide();
return; return;
} }
$(el).toggle(!!( // !! to avoid toggle overloads with different behavior this.$widget.find(el).toggle(!!( // !! to avoid toggle overloads with different behavior
action.actionName.toLowerCase().includes(filter) action.actionName.toLowerCase().includes(filter)
|| action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter)) || action.defaultShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))
|| action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter)) || action.effectiveShortcuts.some(shortcut => shortcut.toLowerCase().includes(filter))

View File

@ -0,0 +1,55 @@
import utils from "../../../services/utils.js";
import OptionsTab from "./options_tab.js";
const TPL = `
<div class="options-section">
<h4>Spell check</h4>
<p>These options apply only for desktop builds, browsers will use their own native spell check. App restart is required after change.</p>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="spell-check-enabled">
<label class="custom-control-label" for="spell-check-enabled">Enable spellcheck</label>
</div>
<br/>
<div class="form-group">
<label for="spell-check-language-code">Language code(s)</label>
<input type="text" class="form-control" id="spell-check-language-code" placeholder="for example &quot;en-US&quot;, &quot;de-AT&quot;">
</div>
<p>Multiple languages can be separated by comma, e.g. <code>en-US, de-DE, cs</code>. Changes to the spell check options will take effect after application restart.</p>
<p><strong>Available language codes: </strong> <span id="available-language-codes"></span></p>
</div>`;
export default class SpellcheckOptions extends OptionsTab {
get tabTitle() { return "Spellcheck" }
lazyRender() {
this.$widget = $(TPL);
this.$spellCheckEnabled = this.$widget.find("#spell-check-enabled");
this.$spellCheckLanguageCode = this.$widget.find("#spell-check-language-code");
this.$spellCheckEnabled.on('change', () =>
this.updateCheckboxOption('spellCheckEnabled', this.$spellCheckEnabled));
this.$spellCheckLanguageCode.on('change', () =>
this.updateOption('spellCheckLanguageCode', this.$spellCheckLanguageCode.val()));
this.$availableLanguageCodes = this.$widget.find("#available-language-codes");
if (utils.isElectron()) {
const { webContents } = utils.dynamicRequire('@electron/remote').getCurrentWindow();
this.$availableLanguageCodes.text(webContents.session.availableSpellCheckerLanguages.join(', '));
}
}
optionsLoaded(options) {
this.setCheckboxState(this.$spellCheckEnabled, options.spellCheckEnabled);
this.$spellCheckLanguageCode.val(options.spellCheckLanguageCode);
}
}

View File

@ -1,51 +1,56 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js"; import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<h4 style="margin-top: 0px;">Sync configuration</h4> <div class="options-section">
<h4 style="margin-top: 0px;">Sync configuration</h4>
<form id="sync-setup-form">
<div class="form-group">
<label for="sync-server-host">Server instance address</label>
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>">
</div>
<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" style="text-align: left;">
</div>
<div class="form-group">
<label for="sync-proxy">Sync proxy server (optional)</label>
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only)</p>
</div>
<div style="display: flex; justify-content: space-between;">
<button class="btn btn-primary">Save</button>
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
</div>
<form id="sync-setup-form"> <div class="options-section">
<div class="form-group"> <h4>Sync test</h4>
<label for="sync-server-host">Server instance address</label>
<input class="form-control" id="sync-server-host" placeholder="https://<host>:<port>"> <p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p>
</div>
<button id="test-sync-button" class="btn">Test sync</button>
</div>`;
<div class="form-group"> export default class SyncOptions extends OptionsTab {
<label for="sync-server-timeout">Sync timeout (milliseconds)</label> get tabTitle() { return "Sync" }
<input class="form-control" id="sync-server-timeout" min="1" max="10000000" type="number" style="text-align: left;">
</div>
<div class="form-group"> lazyRender() {
<label for="sync-proxy">Sync proxy server (optional)</label> this.$widget = $(TPL);
<input class="form-control" id="sync-proxy" placeholder="https://<host>:<port>">
<p><strong>Note:</strong> If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only)</p> this.$form = this.$widget.find("#sync-setup-form");
</div> this.$syncServerHost = this.$widget.find("#sync-server-host");
this.$syncServerTimeout = this.$widget.find("#sync-server-timeout");
<div style="display: flex; justify-content: space-between;"> this.$syncProxy = this.$widget.find("#sync-proxy");
<button class="btn btn-primary">Save</button> this.$testSyncButton = this.$widget.find("#test-sync-button");
<button class="btn" type="button" data-help-page="Synchronization">Help</button>
</div>
</form>
<br/>
<h4>Sync test</h4>
<p>This will test the connection and handshake to the sync server. If the sync server isn't initialized, this will set it up to sync with the local document.</p>
<button id="test-sync-button" class="btn">Test sync</button>`;
export default class SyncOptions {
constructor() {
$("#options-sync-setup").html(TPL);
this.$form = $("#sync-setup-form");
this.$syncServerHost = $("#sync-server-host");
this.$syncServerTimeout = $("#sync-server-timeout");
this.$syncProxy = $("#sync-proxy");
this.$testSyncButton = $("#test-sync-button");
this.$form.on('submit', () => this.save()); this.$form.on('submit', () => this.save());
@ -62,19 +67,17 @@ export default class SyncOptions {
} }
optionsLoaded(options) { optionsLoaded(options) {
this.$syncServerHost.val(options['syncServerHost']); this.$syncServerHost.val(options.syncServerHost);
this.$syncServerTimeout.val(options['syncServerTimeout']); this.$syncServerTimeout.val(options.syncServerTimeout);
this.$syncProxy.val(options['syncProxy']); this.$syncProxy.val(options.syncProxy);
} }
save() { save() {
const opts = { this.updateMultipleOptions({
'syncServerHost': this.$syncServerHost.val(), 'syncServerHost': this.$syncServerHost.val(),
'syncServerTimeout': this.$syncServerTimeout.val(), 'syncServerTimeout': this.$syncServerTimeout.val(),
'syncProxy': this.$syncProxy.val() 'syncProxy': this.$syncProxy.val()
}; });
server.put('options', opts).then(() => toastService.showMessage("Options changed have been saved."));
return false; return false;
} }

View File

@ -1,18 +1,20 @@
import server from "../../../services/server.js"; import server from "../../../services/server.js";
import toastService from "../../../services/toast.js";
import OptionsTab from "./options_tab.js";
const TPL = ` const TPL = `
<p><strong>Settings on this options tab are saved automatically after each change.</strong></p> <p><strong>Settings on this options tab are saved automatically after each change.</strong></p>
<form> <div class="options-section">
<h4>Heading style</h4> <h4>Heading style</h4>
<select class="form-control" id="heading-style"> <select class="form-control" id="heading-style">
<option value="plain">Plain</option> <option value="plain">Plain</option>
<option value="underline">Underline</option> <option value="underline">Underline</option>
<option value="markdown">Markdown-style</option> <option value="markdown">Markdown-style</option>
</select> </select>
</div>
<br/> <div class="options-section">
<h4>Table of contents</h4> <h4>Table of contents</h4>
Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number: Table of contents will appear in text notes when the note has more than a defined number of headings. You can customize this number:
@ -22,29 +24,42 @@ const TPL = `
</div> </div>
<p>You can also use this option to effectively disable TOC by setting a very high number.</p> <p>You can also use this option to effectively disable TOC by setting a very high number.</p>
</form>`; </div>
<div class="options-section">
<h4>Automatic readonly size</h4>
export default class TextNotesOptions { <p>Automatic readonly note size is the size after which notes will be displayed in a readonly mode (for performance reasons).</p>
constructor() {
$("#options-text-notes").html(TPL);
<div class="form-group">
<label for="auto-readonly-size-text">Automatic readonly size (text notes)</label>
<input class="form-control" id="auto-readonly-size-text" type="number" min="0" style="text-align: right;">
</div>
</div>`;
export default class TextNotesOptions extends OptionsTab {
get tabTitle() { return "Text notes" }
lazyRender() {
this.$widget = $(TPL);
this.$body = $("body"); this.$body = $("body");
this.$headingStyle = $("#heading-style"); this.$headingStyle = this.$widget.find("#heading-style");
this.$headingStyle.on('change', () => { this.$headingStyle.on('change', () => {
const newHeadingStyle = this.$headingStyle.val(); const newHeadingStyle = this.$headingStyle.val();
this.toggleBodyClass("heading-style-", newHeadingStyle); this.toggleBodyClass("heading-style-", newHeadingStyle);
server.put('options/headingStyle/' + newHeadingStyle); this.updateOption('headingStyle', newHeadingStyle);
}); });
this.$minTocHeadings = $("#min-toc-headings"); this.$minTocHeadings = this.$widget.find("#min-toc-headings");
this.$minTocHeadings.on('change', () => { this.$minTocHeadings.on('change', () =>
const minTocHeadings = this.$minTocHeadings.val(); this.updateOption('minTocHeadings', this.$minTocHeadings.val()));
server.put('options/minTocHeadings/' + minTocHeadings); this.$autoReadonlySizeText = this.$widget.find("#auto-readonly-size-text");
}); this.$autoReadonlySizeText.on('change', () =>
this.updateOption('autoReadonlySizeText', this.$autoReadonlySizeText.val()));
} }
toggleBodyClass(prefix, value) { toggleBodyClass(prefix, value) {
@ -57,8 +72,9 @@ export default class TextNotesOptions {
this.$body.addClass(prefix + value); this.$body.addClass(prefix + value);
} }
async optionsLoaded(options) { optionsLoaded(options) {
this.$headingStyle.val(options.headingStyle); this.$headingStyle.val(options.headingStyle);
this.$minTocHeadings.val(options.minTocHeadings); this.$minTocHeadings.val(options.minTocHeadings);
this.$autoReadonlySizeText.val(options.autoReadonlySizeText);
} }
} }

View File

@ -28,7 +28,7 @@ export default class PasswordNoteSetDialog extends BasicWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button"); this.$openPasswordOptionsButton = this.$widget.find(".open-password-options-button");
this.$openPasswordOptionsButton.on("click", () => { this.$openPasswordOptionsButton.on("click", () => {
this.triggerCommand("showOptions", { openTab: 'password' }); this.triggerCommand("showOptions", { openTab: 'PasswordOptions' });
}); });
} }

View File

@ -7,14 +7,19 @@ import appContext from "../../services/app_context.js";
import hoistedNoteService from "../../services/hoisted_note.js"; import hoistedNoteService from "../../services/hoisted_note.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
import dialogService from "../dialog.js"; import dialogService from "../dialog.js";
import toastService from "../../services/toast.js";
const TPL = ` const TPL = `
<div class="recent-changes-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="recent-changes-dialog modal fade mx-auto" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-lg modal-dialog-scrollable" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Recent changes</h5> <h5 class="modal-title mr-auto">Recent changes</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<button class="erase-deleted-notes-now-button btn btn-xs" style="padding: 0 10px">
Erase deleted notes now</button>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
@ -29,20 +34,30 @@ export default class RecentChangesDialog extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$content = this.$widget.find(".recent-changes-content"); this.$content = this.$widget.find(".recent-changes-content");
this.$eraseDeletedNotesNow = this.$widget.find(".erase-deleted-notes-now-button");
this.$eraseDeletedNotesNow.on("click", () => {
server.post('notes/erase-deleted-notes-now').then(() => {
this.refresh();
toastService.showMessage("Deleted notes have been erased.");
});
});
} }
async showRecentChangesEvent({ancestorNoteId}) { async showRecentChangesEvent({ancestorNoteId}) {
await this.refresh(ancestorNoteId); this.ancestorNoteId = ancestorNoteId;
await this.refresh();
utils.openDialog(this.$widget); utils.openDialog(this.$widget);
} }
async refresh(ancestorNoteId) { async refresh() {
if (!ancestorNoteId) { if (!this.ancestorNoteId) {
ancestorNoteId = hoistedNoteService.getHoistedNoteId(); this.ancestorNoteId = hoistedNoteService.getHoistedNoteId();
} }
const recentChangesRows = await server.get('recent-changes/' + ancestorNoteId); const recentChangesRows = await server.get('recent-changes/' + this.ancestorNoteId);
// preload all notes into cache // preload all notes into cache
await froca.getNotes(recentChangesRows.map(r => r.noteId), true); await froca.getNotes(recentChangesRows.map(r => r.noteId), true);

View File

@ -142,26 +142,27 @@ export default class FindWidget extends NoteContextAwareWidget {
return; return;
} }
if (!['text', 'code', 'render'].includes(this.note.type) || !this.$findBox.is(":hidden")) { if (!['text', 'code', 'render'].includes(this.note.type)) {
return; return;
} }
this.handler = await this.getHandler();
this.$findBox.show(); this.$findBox.show();
this.$input.focus(); this.$input.focus();
this.$totalFound.text(0); this.handler = await this.getHandler();
this.$currentFound.text(0);
const searchTerm = await this.handler.getInitialSearchTerm(); const isAlreadyVisible = this.$findBox.is(":visible");
this.$input.val(searchTerm || ""); if (isAlreadyVisible) {
// Directly perform the search if there's some text to
// find, without delaying or waiting for enter
if (searchTerm !== "") {
this.$input.select(); this.$input.select();
await this.performFind(); } else {
this.$totalFound.text(0);
this.$currentFound.text(0);
const searchTerm = await this.handler.getInitialSearchTerm();
this.$input.val(searchTerm || "");
if (searchTerm !== "") {
this.$input.select();
await this.performFind();
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -243,15 +243,17 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
$promotedAttributes = (await attributeRenderer.renderNormalAttributes(this.note)).$renderedAttributes; $promotedAttributes = (await attributeRenderer.renderNormalAttributes(this.note)).$renderedAttributes;
} }
const {assetPath} = window.glob;
this.$widget.find('.note-detail-printable:visible').printThis({ this.$widget.find('.note-detail-printable:visible').printThis({
header: $("<div>") header: $("<div>")
.append($("<h2>").text(this.note.title)) .append($("<h2>").text(this.note.title))
.append($promotedAttributes) .append($promotedAttributes)
.prop('outerHTML'), .prop('outerHTML'),
footer: ` footer: `
<script src="libraries/katex/katex.min.js"></script> <script src="${assetPath}/libraries/katex/katex.min.js"></script>
<script src="libraries/katex/mhchem.min.js"></script> <script src="${assetPath}/libraries/katex/mhchem.min.js"></script>
<script src="libraries/katex/auto-render.min.js"></script> <script src="${assetPath}/libraries/katex/auto-render.min.js"></script>
<script> <script>
document.body.className += ' ck-content printed-content'; document.body.className += ' ck-content printed-content';
@ -260,13 +262,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
`, `,
importCSS: false, importCSS: false,
loadCSS: [ loadCSS: [
"libraries/codemirror/codemirror.css", assetPath + "/libraries/codemirror/codemirror.css",
"libraries/ckeditor/ckeditor-content.css", assetPath + "/libraries/ckeditor/ckeditor-content.css",
"libraries/bootstrap/css/bootstrap.min.css", assetPath + "/libraries/bootstrap/css/bootstrap.min.css",
"libraries/katex/katex.min.css", assetPath + "/libraries/katex/katex.min.css",
"stylesheets/print.css", assetPath + "/stylesheets/print.css",
"stylesheets/relation_map.css", assetPath + "/stylesheets/relation_map.css",
"stylesheets/ckeditor-theme.css" assetPath + "/stylesheets/ckeditor-theme.css"
], ],
debug: true debug: true
}); });
@ -283,7 +285,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
// globally, so it gets also to e.g. ribbon components. But this means that the event can be generated multiple // globally, so it gets also to e.g. ribbon components. But this means that the event can be generated multiple
// times if the same note is open in several tabs. // times if the same note is open in several tabs.
if (loadResults.isNoteReloaded(this.noteId, this.componentId) if (loadResults.isNoteContentReloaded(this.noteId, this.componentId)) {
// probably incorrect event
// calling this.refresh() is not enough since the event needs to be propagated to children as well
// FIXME: create a separate event to force hierarchical refresh
this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId});
}
else if (loadResults.isNoteReloaded(this.noteId, this.componentId)
&& (this.type !== await this.getWidgetType() || this.mime !== this.note.mime)) { && (this.type !== await this.getWidgetType() || this.mime !== this.note.mime)) {
this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId}); this.triggerEvent('noteTypeMimeChanged', {noteId: this.noteId});

View File

@ -163,13 +163,17 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
const {icons} = (await import('./icon_list.js')).default; const {icons} = (await import('./icon_list.js')).default;
search = search?.trim()?.toLowerCase();
for (const icon of icons) { for (const icon of icons) {
if (categoryId && icon.category_id !== categoryId) { if (categoryId && icon.category_id !== categoryId) {
continue; continue;
} }
if (search && search.trim() && !icon.name.includes(search.trim().toLowerCase())) { if (search) {
continue; if (!icon.name.includes(search) && !icon.term?.find(t => t.includes(search))) {
continue;
}
} }
this.$iconList.append( this.$iconList.append(

View File

@ -86,8 +86,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
this.mapType = this.note.getLabelValue("mapType") === "tree" ? "tree" : "link"; this.mapType = this.note.getLabelValue("mapType") === "tree" ? "tree" : "link";
this.setDimensions();
await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH); await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH);
this.graph = ForceGraph()(this.$container[0]) this.graph = ForceGraph()(this.$container[0])
@ -121,7 +119,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
.linkCanvasObjectMode(() => "after"); .linkCanvasObjectMode(() => "after");
} }
let mapRootNoteId = this.getMapRootNoteId(); const mapRootNoteId = this.getMapRootNoteId();
const data = await this.loadNotesAndRelations(mapRootNoteId); const data = await this.loadNotesAndRelations(mapRootNoteId);
const nodeLinkRatio = data.nodes.length / data.links.length; const nodeLinkRatio = data.nodes.length / data.links.length;
@ -264,7 +262,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
return { return {
nodes: this.nodes, nodes: this.nodes,
links: links.map(link => ({ links: links.map(link => ({
id: link.id, id: `${link.sourceNoteId}-${link.targetNoteId}`,
source: link.sourceNoteId, source: link.sourceNoteId,
target: link.targetNoteId, target: link.targetNoteId,
name: link.names.join(", ") name: link.names.join(", ")
@ -331,8 +329,10 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
renderData(data) { renderData(data) {
this.graph.graphData(data); this.graph.graphData(data);
if (this.widgetMode === 'ribbon') { if (this.widgetMode === 'ribbon' && this.note?.type !== 'search') {
setTimeout(() => { setTimeout(() => {
this.setDimensions();
const subGraphNoteIds = this.getSubGraphConnectedToCurrentNote(data); const subGraphNoteIds = this.getSubGraphConnectedToCurrentNote(data);
this.graph.zoomToFit(400, 50, node => subGraphNoteIds.has(node.id)); this.graph.zoomToFit(400, 50, node => subGraphNoteIds.has(node.id));
@ -342,12 +342,18 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
} }
}, 1000); }, 1000);
} }
else if (this.widgetMode === 'type') { else {
if (data.nodes.length > 1) { if (data.nodes.length > 1) {
setTimeout(() => { setTimeout(() => {
this.graph.zoomToFit(400, 10); this.setDimensions();
if (data.nodes.length < 30) { const noteIdsWithLinks = this.getNoteIdsWithLinks(data);
if (noteIdsWithLinks.size > 0) {
this.graph.zoomToFit(400, 30, node => noteIdsWithLinks.has(node.id));
}
if (noteIdsWithLinks.size < 30) {
this.graph.d3VelocityDecay(0.4); this.graph.d3VelocityDecay(0.4);
} }
}, 1000); }, 1000);
@ -355,6 +361,17 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
} }
} }
getNoteIdsWithLinks(data) {
const noteIds = new Set();
for (const link of data.links) {
noteIds.add(link.source.id);
noteIds.add(link.target.id);
}
return noteIds;
}
getSubGraphConnectedToCurrentNote(data) { getSubGraphConnectedToCurrentNote(data) {
function getGroupedLinks(links, type) { function getGroupedLinks(links, type) {
const map = {}; const map = {};
@ -398,7 +415,12 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
} }
entitiesReloadedEvent({loadResults}) { entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes(this.componentId).find(attr => attr.name === 'mapType' && attributeService.isAffecting(attr, this.note))) { if (loadResults.getAttributes(this.componentId).find(
attr =>
attr.type === 'label'
&& ['mapType', 'mapRootNoteId'].includes(attr.name)
&& attributeService.isAffecting(attr, this.note)
)) {
this.refresh(); this.refresh();
} }
} }

View File

@ -347,7 +347,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
node.setFocus(true); node.setFocus(true);
} }
else if (event.ctrlKey) { else if ((!utils.isMac() && event.ctrlKey) || (utils.isMac() && event.metaKey)) {
const notePath = treeService.getNotePath(node); const notePath = treeService.getNotePath(node);
appContext.tabManager.openTabWithNoteWithHoisting(notePath); appContext.tabManager.openTabWithNoteWithHoisting(notePath);
} }
@ -1483,7 +1483,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clipboard.pasteInto(node.data.branchId); clipboard.pasteInto(node.data.branchId);
} }
pasteNotesAfterFromClipboard({node}) { pasteNotesAfterFromClipboardCommand({node}) {
clipboard.pasteAfter(node.data.branchId); clipboard.pasteAfter(node.data.branchId);
} }

View File

@ -28,7 +28,7 @@ const TPL = `
<button class="image-open btn btn-sm btn-primary" type="button">Open</button> <button class="image-open btn btn-sm btn-primary" type="button">Open</button>
<button class="image-copy-to-clipboard btn btn-sm btn-primary" type="button">Copy to clipboard</button> <button class="image-copy-reference-to-clipboard btn btn-sm btn-primary" type="button">Copy reference to clipboard</button>
<button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button> <button class="image-upload-new-revision btn btn-sm btn-primary" type="button">Upload new revision</button>
</div> </div>
@ -61,7 +61,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.contentSized(); this.contentSized();
this.$copyToClipboardButton = this.$widget.find(".image-copy-to-clipboard"); this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard");
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision"); this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input"); this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
this.$fileName = this.$widget.find(".image-filename"); this.$fileName = this.$widget.find(".image-filename");
@ -74,7 +74,7 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
this.$imageDownloadButton = this.$widget.find(".image-download"); this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId)); this.$imageDownloadButton.on('click', () => openService.downloadFileNote(this.noteId));
this.$copyToClipboardButton.on('click', () => this.triggerEvent(`copyImageToClipboard`, {ntxId: this.noteContext.ntxId})); this.$copyReferenceToClipboardButton.on('click', () => this.triggerEvent(`copyImageReferenceToClipboard`, {ntxId: this.noteContext.ntxId}));
this.$uploadNewRevisionButton.on("click", () => { this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click"); this.$uploadNewRevisionInput.trigger("click");
@ -121,7 +121,5 @@ export default class ImagePropertiesWidget extends NoteContextAwareWidget {
this.$fileName.text(attributeMap.originalFileName || "?"); this.$fileName.text(attributeMap.originalFileName || "?");
this.$fileSize.text(noteComplement.contentLength + " bytes"); this.$fileSize.text(noteComplement.contentLength + " bytes");
this.$fileType.text(note.mime); this.$fileType.text(note.mime);
const imageHash = utils.randomString(10);
} }
} }

View File

@ -16,7 +16,6 @@ const TPL = `
position: absolute; position: absolute;
right: 5px; right: 5px;
bottom: 5px; bottom: 5px;
font-size: 180%;
z-index: 1000; z-index: 1000;
} }

Some files were not shown because too many files have changed in this diff Show More