mirror of
https://github.com/zadam/trilium.git
synced 2025-10-29 17:26:38 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d616a77d6b | ||
|
|
5b679930de | ||
|
|
067ca9ab16 | ||
|
|
70708b36ef | ||
|
|
fb3d5f25ac | ||
|
|
9d7d79ef94 | ||
|
|
ba33a0d330 | ||
|
|
aea81c9872 | ||
|
|
806ab22fa8 | ||
|
|
9c2b98915e | ||
|
|
f2ca9276d6 | ||
|
|
48b697f408 | ||
|
|
e1e185e5db | ||
|
|
e06c5703ee | ||
|
|
fe3bb2c5f6 | ||
|
|
6afc299efb | ||
|
|
369274ead7 | ||
|
|
04e6431c09 | ||
|
|
e89057a771 | ||
|
|
4f27254e64 | ||
|
|
577dc95ab8 | ||
|
|
a266d6a3d5 | ||
|
|
749b6cb57e | ||
|
|
b0b2951ff6 | ||
|
|
1f3d73b9fd | ||
|
|
bdfd760b9d | ||
|
|
7133e60267 | ||
|
|
fc4edf4aa7 | ||
|
|
eaf93a70cd | ||
|
|
b093569ec5 | ||
|
|
4633c68a0c | ||
|
|
33571e0ef3 | ||
|
|
31876d2cf9 | ||
|
|
81c6043cb6 | ||
|
|
1982d054ef | ||
|
|
e56979c482 | ||
|
|
58555b3660 | ||
|
|
b7b1324dd0 | ||
|
|
e318acc977 | ||
|
|
8ae82f5b69 | ||
|
|
26442f418a | ||
|
|
23a432e7d8 | ||
|
|
984ecaf99c | ||
|
|
21b73a86b2 | ||
|
|
7d8277699c | ||
|
|
928ed7a034 |
@@ -1,10 +1,13 @@
|
||||
[General]
|
||||
# Instance name can be used to distinguish between different instances
|
||||
# Instance name can be used to distinguish between different instances using backend api.getInstanceName()
|
||||
instanceName=
|
||||
|
||||
# set to true to allow using Trilium without authentication (makes sense for server build only, desktop build doesn't need password)
|
||||
noAuthentication=false
|
||||
|
||||
# set to true to disable backups (e.g. because of limited space on server)
|
||||
noBackup=false
|
||||
|
||||
# Disable automatically generating desktop icon
|
||||
# noDesktopIcon=true
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
|
||||
|
||||
.ck-widget__selection-handle, .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
|
||||
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.45.4",
|
||||
"version": "0.45.6",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2654,9 +2654,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.4.tgz",
|
||||
"integrity": "sha512-OHP8qMKgW8D8GtH+altB22WJw/lBOyyVdoz5e8D0/iPBmJU3Jm93vO4z4Eh/9DvdSXlH8bMHUCMLL9PVW6f+tw==",
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.5.tgz",
|
||||
"integrity": "sha512-EPmDsp7sO0UPtw7nLD1ufse/nBskP+ifXzBgUg9psCUlapkzuwYi6pmLAzKLW/bVjwgyUKwh1OKWILWfOeLGcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@electron/get": "^1.0.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.45.5",
|
||||
"version": "0.45.10",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"bin": {
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "7.0.2",
|
||||
"electron": "9.3.4",
|
||||
"electron": "9.3.5",
|
||||
"electron-builder": "22.9.1",
|
||||
"electron-packager": "15.1.0",
|
||||
"electron-rebuild": "2.3.2",
|
||||
|
||||
@@ -51,6 +51,12 @@ const TPL = `
|
||||
<label for="erase-notes-after-time-in-seconds">Erase notes after X seconds</label>
|
||||
<input class="form-control" id="erase-notes-after-time-in-seconds" type="number" min="0">
|
||||
</div>
|
||||
|
||||
<p>You can also trigger erasing manually:</p>
|
||||
|
||||
<button id="erase-deleted-notes-now-button" class="btn">Erase deleted notes now</button>
|
||||
|
||||
<br/><br/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -117,6 +123,13 @@ export default class ProtectedSessionOptions {
|
||||
return false;
|
||||
});
|
||||
|
||||
this.$eraseDeletedNotesButton = $("#erase-deleted-notes-now-button");
|
||||
this.$eraseDeletedNotesButton.on('click', () => {
|
||||
server.post('notes/erase-deleted-notes-now').then(() => {
|
||||
toastService.showMessage("Deleted notes have been erased.");
|
||||
});
|
||||
});
|
||||
|
||||
this.$protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
|
||||
|
||||
this.$protectedSessionTimeout.on('change', () => {
|
||||
|
||||
@@ -75,14 +75,16 @@ class NoteShort {
|
||||
this.parentToBranch[parentNoteId] = branchId;
|
||||
}
|
||||
|
||||
addChild(childNoteId, branchId) {
|
||||
addChild(childNoteId, branchId, sort = true) {
|
||||
if (!this.children.includes(childNoteId)) {
|
||||
this.children.push(childNoteId);
|
||||
}
|
||||
|
||||
this.childToBranch[childNoteId] = branchId;
|
||||
|
||||
this.sortChildren();
|
||||
if (sort) {
|
||||
this.sortChildren();
|
||||
}
|
||||
}
|
||||
|
||||
sortChildren() {
|
||||
|
||||
@@ -130,7 +130,7 @@ function linkContextMenu(e) {
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
}
|
||||
else if (command === 'openNoteInNewWindow') {
|
||||
appContext.openInNewWindow(notePath);
|
||||
appContext.triggerCommand('openInWindow', {notePath});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,8 +14,17 @@ function setupSplit(left, right) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftPaneWidth = options.getInt('leftPaneWidth');
|
||||
const rightPaneWidth = options.getInt('rightPaneWidth');
|
||||
let leftPaneWidth = options.getInt('leftPaneWidth');
|
||||
if (!leftPaneWidth || leftPaneWidth < 5) {
|
||||
leftPaneWidth = 5;
|
||||
}
|
||||
|
||||
let rightPaneWidth = options.getInt('rightPaneWidth');
|
||||
if (!rightPaneWidth || rightPaneWidth < 5) {
|
||||
rightPaneWidth = 5;
|
||||
}
|
||||
|
||||
console.log(leftPaneWidth, rightPaneWidth);
|
||||
|
||||
if (left && right) {
|
||||
instance = Split(['#left-pane', '#center-pane', '#right-pane'], {
|
||||
@@ -49,4 +58,4 @@ function setupSplit(left, right) {
|
||||
|
||||
export default {
|
||||
setupSplit
|
||||
};
|
||||
};
|
||||
|
||||
@@ -8,8 +8,8 @@ async function syncNow() {
|
||||
toastService.showMessage("Sync finished successfully.");
|
||||
}
|
||||
else {
|
||||
if (result.message.length > 100) {
|
||||
result.message = result.message.substr(0, 100);
|
||||
if (result.message.length > 200) {
|
||||
result.message = result.message.substr(0, 200) + "...";
|
||||
}
|
||||
|
||||
toastService.showError("Sync failed: " + result.message);
|
||||
|
||||
@@ -87,6 +87,8 @@ class TreeCache {
|
||||
const branchRows = resp.branches;
|
||||
const attributeRows = resp.attributes;
|
||||
|
||||
const noteIdsToSort = new Set();
|
||||
|
||||
for (const noteRow of noteRows) {
|
||||
const {noteId} = noteRow;
|
||||
|
||||
@@ -153,7 +155,9 @@ class TreeCache {
|
||||
const parentNote = this.notes[branch.parentNoteId];
|
||||
|
||||
if (parentNote) {
|
||||
parentNote.addChild(branch.noteId, branch.branchId);
|
||||
parentNote.addChild(branch.noteId, branch.branchId, false);
|
||||
|
||||
noteIdsToSort.add(parentNote.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +182,11 @@ class TreeCache {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort all of them at once, this avoids repeated sorts (#1480)
|
||||
for (const noteId of noteIdsToSort) {
|
||||
this.notes[noteId].sortChildren();
|
||||
}
|
||||
}
|
||||
|
||||
async reloadNotes(noteIds) {
|
||||
|
||||
@@ -2,9 +2,9 @@ import treeService from './tree.js';
|
||||
import treeCache from "./tree_cache.js";
|
||||
import hoistedNoteService from './hoisted_note.js';
|
||||
import clipboard from './clipboard.js';
|
||||
import protectedSessionHolder from "./protected_session_holder.js";
|
||||
import noteCreateService from "./note_create.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import appContext from "./app_context.js";
|
||||
|
||||
class TreeContextMenu {
|
||||
/**
|
||||
@@ -95,7 +95,7 @@ class TreeContextMenu {
|
||||
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
|
||||
{ title: 'Paste after', command: "pasteNotesAfterFromClipboard", uiIcon: "paste",
|
||||
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
|
||||
{ title: "Duplicate subtree(s) here", command: "duplicateSubtree", uiIcon: "empty",
|
||||
{ title: `Duplicate subtree <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "empty",
|
||||
enabled: parentNotSearch && isNotRoot && !isHoisted },
|
||||
{ title: "----" },
|
||||
{ title: "Export", command: "exportNote", uiIcon: "empty",
|
||||
@@ -110,14 +110,7 @@ class TreeContextMenu {
|
||||
const notePath = treeService.getNotePath(this.node);
|
||||
|
||||
if (command === 'openInTab') {
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
await this.node.load(true);
|
||||
|
||||
console.log("Reload took", Date.now() - start, "ms");
|
||||
|
||||
// appContext.tabManager.openTabWithNote(notePath);
|
||||
appContext.tabManager.openTabWithNote(notePath);
|
||||
}
|
||||
else if (command === "insertNoteAfter") {
|
||||
const parentNoteId = this.node.data.parentNoteId;
|
||||
|
||||
@@ -234,6 +234,8 @@ export default class AttributeDetailWidget extends TabAwareWidget {
|
||||
|
||||
this.$inputName = this.$widget.find('.attr-input-name');
|
||||
this.$inputName.on('keyup', () => this.userEditedAttribute());
|
||||
this.$inputName.on('change', () => this.userEditedAttribute());
|
||||
this.$inputName.on('autocomplete:closed', () => this.userEditedAttribute());
|
||||
|
||||
this.$inputName.on('focus', () => {
|
||||
attributeAutocompleteService.initAttributeNameAutocomplete({
|
||||
|
||||
@@ -214,7 +214,8 @@ export default class AttributeListWidget extends TabAwareWidget {
|
||||
noteId: attribute.noteId,
|
||||
type: attribute.type,
|
||||
name: attribute.name,
|
||||
value: attribute.value
|
||||
value: attribute.value,
|
||||
isInheritable: attribute.isInheritable
|
||||
},
|
||||
isOwned: false,
|
||||
x: e.pageX,
|
||||
|
||||
@@ -248,13 +248,22 @@ export default class NoteDetailWidget extends TabAwareWidget {
|
||||
|
||||
this.$widget.find('.note-detail-printable:visible').printThis({
|
||||
header: $("<h2>").text(this.note && this.note.title).prop('outerHTML'),
|
||||
footer: "<script>document.body.className += ' ck-content';</script>",
|
||||
footer: `
|
||||
<script src="libraries/katex/katex.min.js"></script>
|
||||
<script src="libraries/katex/auto-render.min.js"></script>
|
||||
<script>
|
||||
document.body.className += ' ck-content printed-content';
|
||||
|
||||
renderMathInElement(document.body, {});
|
||||
</script>
|
||||
`,
|
||||
importCSS: false,
|
||||
loadCSS: [
|
||||
"libraries/codemirror/codemirror.css",
|
||||
"libraries/ckeditor/ckeditor-content.css",
|
||||
"libraries/ckeditor/ckeditor-content.css",
|
||||
"libraries/bootstrap/css/bootstrap.min.css",
|
||||
"libraries/katex/katex.min.css",
|
||||
"stylesheets/print.css",
|
||||
"stylesheets/relation_map.css",
|
||||
"stylesheets/themes.css"
|
||||
|
||||
@@ -59,6 +59,7 @@ export default class NotePathsWidget extends TabAwareWidget {
|
||||
|
||||
this.$currentPath = this.$widget.find('.current-path');
|
||||
this.$dropdown = this.$widget.find(".dropdown");
|
||||
this.$dropdownToggle = this.$widget.find('.dropdown-toggle');
|
||||
|
||||
this.$notePathList = this.$dropdown.find(".note-path-list");
|
||||
|
||||
@@ -100,6 +101,8 @@ export default class NotePathsWidget extends TabAwareWidget {
|
||||
|
||||
parentNoteId = noteId;
|
||||
}
|
||||
|
||||
this.$dropdownToggle.dropdown('hide');
|
||||
}
|
||||
|
||||
async renderDropdown() {
|
||||
@@ -141,20 +144,20 @@ export default class NotePathsWidget extends TabAwareWidget {
|
||||
async addPath(notePath, isCurrent) {
|
||||
const title = await treeService.getNotePathTitle(notePath);
|
||||
|
||||
const noteLink = await linkService.createNoteLink(notePath, {title});
|
||||
const $noteLink = await linkService.createNoteLink(notePath, {title});
|
||||
|
||||
noteLink
|
||||
$noteLink
|
||||
.addClass("dropdown-item");
|
||||
|
||||
noteLink
|
||||
$noteLink
|
||||
.find('a')
|
||||
.addClass("no-tooltip-preview");
|
||||
|
||||
if (isCurrent) {
|
||||
noteLink.addClass("current");
|
||||
$noteLink.addClass("current");
|
||||
}
|
||||
|
||||
this.$notePathList.append(noteLink);
|
||||
this.$notePathList.append($noteLink);
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({loadResults}) {
|
||||
|
||||
@@ -1198,7 +1198,25 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
this.clearSelectedNodes();
|
||||
}
|
||||
|
||||
canBeMovedUpOrDown(node) {
|
||||
if (node.data.noteId === 'root') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNote = treeCache.getNoteFromCache(node.getParent().data.noteId);
|
||||
|
||||
if (parentNote && parentNote.hasLabel('sorted')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
moveNoteUpCommand({node}) {
|
||||
if (!this.canBeMovedUpOrDown(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const beforeNode = node.getPrevSibling();
|
||||
|
||||
if (beforeNode !== null) {
|
||||
@@ -1207,7 +1225,12 @@ export default class NoteTreeWidget extends TabAwareWidget {
|
||||
}
|
||||
|
||||
moveNoteDownCommand({node}) {
|
||||
if (!this.canBeMovedUpOrDown(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const afterNode = node.getNextSibling();
|
||||
|
||||
if (afterNode !== null) {
|
||||
branchService.moveAfterBranch([node.data.branchId], afterNode.data.branchId);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ export default class NoteTypeWidget extends TabAwareWidget {
|
||||
this.$noteTypeDropdown = this.$widget.find(".note-type-dropdown");
|
||||
this.$noteTypeButton = this.$widget.find(".note-type-button");
|
||||
this.$noteTypeDesc = this.$widget.find(".note-type-desc");
|
||||
|
||||
this.$widget.on('click', '.dropdown-item',
|
||||
() => this.$widget.find('.dropdown-toggle').dropdown('toggle'));
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
@@ -64,8 +67,6 @@ export default class NoteTypeWidget extends TabAwareWidget {
|
||||
const noteType = NOTE_TYPES.find(nt => nt.type === type);
|
||||
|
||||
this.save(noteType.type, noteType.mime);
|
||||
|
||||
this.$widget.find('.dropdown-toggle').dropdown('toggle');
|
||||
});
|
||||
|
||||
if (this.note.type === noteType.type) {
|
||||
|
||||
@@ -38,7 +38,7 @@ const TPL = `
|
||||
cursor: text !important;
|
||||
}
|
||||
|
||||
.note-detail-editable-text *:first-child {
|
||||
.note-detail-editable-text *:not(figure):first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,11 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
async doRefresh(note) {
|
||||
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
|
||||
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
|
||||
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
|
||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||
|
||||
const noteComplement = await treeCache.getNoteComplement(note.noteId);
|
||||
|
||||
this.$content.html(noteComplement.content);
|
||||
|
||||
@@ -703,6 +703,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.include-note.ck-placeholder::before { /* remove placeholder in otherwise empty note */
|
||||
|
||||
@@ -143,6 +143,11 @@ body {
|
||||
--ck-color-dropdown-panel-background: var(--accented-background-color);
|
||||
--ck-color-dropdown-panel-border: var(--main-border-color);
|
||||
|
||||
/* -- Overrides the default .ck-splitbutton class colors. ----------------------------------- */
|
||||
|
||||
--ck-color-split-button-hover-background: var(--ck-color-button-default-hover-background);
|
||||
--ck-color-split-button-hover-border: var(--main-border-color);
|
||||
|
||||
/* -- Overrides the default .ck-input class colors. ----------------------------------------- */
|
||||
|
||||
--ck-color-input-background: var(--accented-background-color);
|
||||
@@ -199,6 +204,9 @@ body {
|
||||
--ck-color-engine-placeholder-text: var(--muted-text-color);
|
||||
|
||||
--ck-z-modal: 10000;
|
||||
|
||||
--ck-color-widget-type-around-button: var(--main-border-color);
|
||||
--ck-color-widget-type-around-button-hover: var(--main-border-color);
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -23,11 +23,7 @@ function exportBranch(req, res) {
|
||||
|
||||
try {
|
||||
if (type === 'subtree' && (format === 'html' || format === 'markdown')) {
|
||||
const start = Date.now();
|
||||
|
||||
zipExportService.exportToZip(taskContext, branch, format, res);
|
||||
|
||||
console.log("Export took", Date.now() - start, "ms");
|
||||
}
|
||||
else if (type === 'single') {
|
||||
singleExportService.exportSingleNote(taskContext, branch, format, res);
|
||||
|
||||
@@ -3,15 +3,33 @@
|
||||
const sql = require('../../services/sql');
|
||||
|
||||
function getRelations(noteIds) {
|
||||
return (sql.getManyRows(`
|
||||
SELECT noteId, name, value AS targetNoteId
|
||||
FROM attributes
|
||||
WHERE (noteId IN (???) OR value IN (???))
|
||||
AND type = 'relation'
|
||||
AND isDeleted = 0
|
||||
AND noteId != ''
|
||||
AND value != ''
|
||||
`, Array.from(noteIds)));
|
||||
noteIds = Array.from(noteIds);
|
||||
|
||||
return [
|
||||
// first read all non-image relations
|
||||
...sql.getManyRows(`
|
||||
SELECT noteId, name, value AS targetNoteId
|
||||
FROM attributes
|
||||
WHERE (noteId IN (???) OR value IN (???))
|
||||
AND type = 'relation'
|
||||
AND name != 'imageLink'
|
||||
AND isDeleted = 0
|
||||
AND noteId != ''
|
||||
AND value != ''`, noteIds),
|
||||
// ... then read only imageLink relations which are not connecting parent and child
|
||||
// this is done to not show image links in the trivial case where they are direct children of the note to which they are included. Same heuristic as in note tree
|
||||
...sql.getManyRows(`
|
||||
SELECT rel.noteId, rel.name, rel.value AS targetNoteId
|
||||
FROM attributes AS rel
|
||||
LEFT JOIN branches ON branches.parentNoteId = rel.noteId AND branches.noteId = rel.value AND branches.isDeleted = 0
|
||||
WHERE (rel.noteId IN (???) OR rel.value IN (???))
|
||||
AND rel.type = 'relation'
|
||||
AND rel.name = 'imageLink'
|
||||
AND rel.isDeleted = 0
|
||||
AND rel.noteId != ''
|
||||
AND rel.value != ''
|
||||
AND branches.branchId IS NULL`, noteIds)
|
||||
];
|
||||
}
|
||||
|
||||
function getLinkMap(req) {
|
||||
|
||||
@@ -193,6 +193,10 @@ function duplicateSubtree(req) {
|
||||
return noteService.duplicateSubtree(noteId, parentNoteId);
|
||||
}
|
||||
|
||||
function eraseDeletedNotesNow() {
|
||||
noteService.eraseDeletedNotesNow();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNote,
|
||||
updateNote,
|
||||
@@ -204,5 +208,6 @@ module.exports = {
|
||||
setNoteTypeMime,
|
||||
getRelationMap,
|
||||
changeTitle,
|
||||
duplicateSubtree
|
||||
duplicateSubtree,
|
||||
eraseDeletedNotesNow
|
||||
};
|
||||
|
||||
@@ -54,11 +54,21 @@ function getBundlesWithLabel(label, value) {
|
||||
}
|
||||
|
||||
function getStartupBundles() {
|
||||
return getBundlesWithLabel("run", "frontendStartup");
|
||||
if (!process.env.TRILIUM_SAFE_MODE) {
|
||||
return getBundlesWithLabel("run", "frontendStartup");
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getWidgetBundles() {
|
||||
return getBundlesWithLabel("widget");
|
||||
if (!process.env.TRILIUM_SAFE_MODE) {
|
||||
return getBundlesWithLabel("widget");
|
||||
}
|
||||
else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getRelationBundles(req) {
|
||||
|
||||
@@ -38,6 +38,8 @@ function saveSyncSeed(req) {
|
||||
}]
|
||||
}
|
||||
|
||||
log.info("Saved sync seed.");
|
||||
|
||||
sqlInit.createDatabaseForSync(options);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ function getTree(req) {
|
||||
const noteIds = sql.getColumn(`
|
||||
WITH RECURSIVE
|
||||
treeWithDescendants(noteId, isExpanded) AS (
|
||||
SELECT noteId, 1 FROM branches WHERE parentNoteId = ? AND isDeleted = 0
|
||||
SELECT noteId, isExpanded FROM branches WHERE parentNoteId = ? AND isDeleted = 0
|
||||
UNION
|
||||
SELECT branches.noteId, branches.isExpanded FROM branches
|
||||
JOIN treeWithDescendants ON branches.parentNoteId = treeWithDescendants.noteId
|
||||
|
||||
@@ -153,6 +153,7 @@ function register(app) {
|
||||
route(GET, '/api/notes/:noteId/revisions/:noteRevisionId/download', [auth.checkApiAuthOrElectron], noteRevisionsApiRoute.downloadNoteRevision);
|
||||
apiRoute(PUT, '/api/notes/:noteId/restore-revision/:noteRevisionId', noteRevisionsApiRoute.restoreNoteRevision);
|
||||
apiRoute(POST, '/api/notes/relation-map', notesApiRoute.getRelationMap);
|
||||
apiRoute(POST, '/api/notes/erase-deleted-notes-now', notesApiRoute.eraseDeletedNotesNow);
|
||||
apiRoute(PUT, '/api/notes/:noteId/change-title', notesApiRoute.changeTitle);
|
||||
apiRoute(POST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2020-11-20T22:50:10+01:00", buildRevision: "e5fa1e0ed555c1c2cb4a14c426d7091d62b5beea" };
|
||||
module.exports = { buildDate:"2021-02-11T23:05:48+01:00", buildRevision: "5b679930de787efc2f70dfe7961cd1677ec2fa0f" };
|
||||
|
||||
@@ -650,7 +650,7 @@ class ConsistencyChecks {
|
||||
// root branch should always be expanded
|
||||
sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
|
||||
|
||||
if (this.unrecoveredConsistencyErrors) {
|
||||
if (!this.unrecoveredConsistencyErrors) {
|
||||
// we run this only if basic checks passed since this assumes basic data consistency
|
||||
|
||||
this.checkTreeCycles();
|
||||
|
||||
@@ -52,6 +52,10 @@ function encrypt(key, plainText, ivLength = 13) {
|
||||
}
|
||||
|
||||
function decrypt(key, cipherText, ivLength = 13) {
|
||||
if (cipherText === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return "[protected]";
|
||||
}
|
||||
@@ -93,6 +97,10 @@ function decrypt(key, cipherText, ivLength = 13) {
|
||||
function decryptString(dataKey, cipherText) {
|
||||
const buffer = decrypt(dataKey, cipherText);
|
||||
|
||||
if (buffer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const str = buffer.toString('utf-8');
|
||||
|
||||
if (str === 'false') {
|
||||
@@ -108,4 +116,4 @@ module.exports = {
|
||||
encrypt,
|
||||
decrypt,
|
||||
decryptString
|
||||
};
|
||||
};
|
||||
|
||||
@@ -143,7 +143,7 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
|
||||
|
||||
// if it's a leaf then we'll export it even if it's empty
|
||||
if (available && ((note.getContent()).length > 0 || childBranches.length === 0)) {
|
||||
if (available && (note.getContent().length > 0 || childBranches.length === 0)) {
|
||||
meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ function exportToZip(taskContext, branch, format, res) {
|
||||
<link rel="stylesheet" href="${cssUrl}">
|
||||
<base target="_parent">
|
||||
</head>
|
||||
<body>
|
||||
<body class="ck-content">
|
||||
<h1>${utils.escapeHtml(title)}</h1>
|
||||
${content}
|
||||
</body>
|
||||
@@ -433,14 +433,13 @@ ${content}
|
||||
}
|
||||
|
||||
const note = branch.getNote();
|
||||
const zipFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".zip";
|
||||
const zipFileName = (branch.prefix ? `${branch.prefix} - ` : "") + note.title + ".zip";
|
||||
|
||||
res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
|
||||
zipFile.end();
|
||||
|
||||
zipFile.outputStream.pipe(res);
|
||||
zipFile.end();
|
||||
|
||||
taskContext.taskSucceeded();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ const scriptService = require('./script');
|
||||
const treeService = require('./tree');
|
||||
const noteService = require('./notes');
|
||||
const repository = require('./repository');
|
||||
const noteCache = require('./note_cache/note_cache');
|
||||
const Attribute = require('../entities/attribute');
|
||||
|
||||
function runAttachedRelations(note, relationName, originEntity) {
|
||||
@@ -22,11 +23,15 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
|
||||
runAttachedRelations(note, 'runOnNoteTitleChange', note);
|
||||
|
||||
if (!note.isRoot()) {
|
||||
const parents = note.getParentNotes();
|
||||
const noteFromCache = noteCache.notes[note.noteId];
|
||||
|
||||
for (const parent of parents) {
|
||||
if (parent.hasOwnedLabel("sorted")) {
|
||||
treeService.sortNotesAlphabetically(parent.noteId);
|
||||
if (!noteFromCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const parentNote of noteFromCache.parents) {
|
||||
if (parentNote.hasLabel("sorted")) {
|
||||
treeService.sortNotesAlphabetically(parentNote.noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,12 +75,26 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) =>
|
||||
if (templateNoteContent) {
|
||||
note.setContent(templateNoteContent);
|
||||
}
|
||||
|
||||
note.type = templateNote.type;
|
||||
note.mime = templateNote.mime;
|
||||
note.save();
|
||||
}
|
||||
|
||||
noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId);
|
||||
}
|
||||
else if (entity.type === 'label' && entity.name === 'sorted') {
|
||||
treeService.sortNotesAlphabetically(entity.noteId);
|
||||
|
||||
if (entity.isInheritable) {
|
||||
const note = noteCache.notes[entity.noteId];
|
||||
|
||||
if (note) {
|
||||
for (const noteId of note.subtreeNoteIds) {
|
||||
treeService.sortNotesAlphabetically(noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (entityName === 'notes') {
|
||||
@@ -90,10 +109,10 @@ eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote
|
||||
function processInverseRelations(entityName, entity, handler) {
|
||||
if (entityName === 'attributes' && entity.type === 'relation') {
|
||||
const note = entity.getNote();
|
||||
const attributes = (note.getOwnedAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
|
||||
const relDefinitions = note.getLabels('relation:' + entity.name);
|
||||
|
||||
for (const attribute of attributes) {
|
||||
const definition = attribute.value;
|
||||
for (const relDefinition of relDefinitions) {
|
||||
const definition = relDefinition.getDefinition();
|
||||
|
||||
if (definition.inverseRelation && definition.inverseRelation.trim()) {
|
||||
const targetNote = entity.getTargetNote();
|
||||
|
||||
@@ -158,7 +158,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
||||
}
|
||||
|
||||
if (taskContext.data.safeImport && attributeService.isAttributeDangerous(attr.type, attr.name)) {
|
||||
attr.name = 'disabled-' + attr.name;
|
||||
attr.name = 'disabled:' + attr.name;
|
||||
}
|
||||
|
||||
attributes.push(attr);
|
||||
|
||||
@@ -184,6 +184,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
description: "Add note above to the selection",
|
||||
scope: "note-tree"
|
||||
},
|
||||
{
|
||||
actionName: "duplicateSubtree",
|
||||
defaultShortcuts: [],
|
||||
description: "Duplicate subtree",
|
||||
scope: "note-tree"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
@@ -307,6 +313,12 @@ const DEFAULT_KEYBOARD_ACTIONS = [
|
||||
description: "Cuts the selection from the current note and creates subnote with the selected text",
|
||||
scope: "text-detail"
|
||||
},
|
||||
{
|
||||
actionName: "addIncludeNoteToText",
|
||||
defaultShortcuts: [],
|
||||
description: "Opens the dialog to include a note",
|
||||
scope: "text-detail"
|
||||
},
|
||||
|
||||
{
|
||||
separator: "Attributes (labels & relations)"
|
||||
|
||||
@@ -133,6 +133,14 @@ class Note {
|
||||
return !!this.attributes.find(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
hasLabel(name) {
|
||||
return this.hasAttribute('label', name);
|
||||
}
|
||||
|
||||
hasRelation(name) {
|
||||
return this.hasAttribute('relation', name);
|
||||
}
|
||||
|
||||
getLabelValue(name) {
|
||||
const label = this.attributes.find(attr => attr.type === 'label' && attr.name === name);
|
||||
|
||||
@@ -275,6 +283,11 @@ class Note {
|
||||
return arr.flat();
|
||||
}
|
||||
|
||||
/** @return {String[]} */
|
||||
get subtreeNoteIds() {
|
||||
return this.subtreeNotes.map(note => note.noteId);
|
||||
}
|
||||
|
||||
get parentCount() {
|
||||
return this.parents.length;
|
||||
}
|
||||
@@ -338,7 +351,7 @@ class Note {
|
||||
|
||||
decrypt() {
|
||||
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
this.title = protectedSessionService.decryptString(note.title);
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
|
||||
this.isDecrypted = true;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"use strict";
|
||||
|
||||
const Note = require('./entities/note');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
|
||||
class NoteCache {
|
||||
constructor() {
|
||||
this.reset();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const NoteRevision = require('../entities/note_revision');
|
||||
const dateUtils = require('../services/date_utils');
|
||||
const log = require('../services/log');
|
||||
|
||||
/**
|
||||
* @param {Note} note
|
||||
@@ -9,14 +10,21 @@ const dateUtils = require('../services/date_utils');
|
||||
function protectNoteRevisions(note) {
|
||||
for (const revision of note.getRevisions()) {
|
||||
if (note.isProtected !== revision.isProtected) {
|
||||
const content = revision.getContent();
|
||||
try {
|
||||
const content = revision.getContent();
|
||||
|
||||
revision.isProtected = note.isProtected;
|
||||
revision.isProtected = note.isProtected;
|
||||
|
||||
// this will force de/encryption
|
||||
revision.setContent(content);
|
||||
// this will force de/encryption
|
||||
revision.setContent(content);
|
||||
|
||||
revision.save();
|
||||
revision.save();
|
||||
}
|
||||
catch (e) {
|
||||
log.error("Could not un/protect note revision ID = " + revision.noteRevisionId);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,18 +185,25 @@ function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
|
||||
}
|
||||
|
||||
function protectNote(note, protect) {
|
||||
if (protect !== note.isProtected) {
|
||||
const content = note.getContent();
|
||||
try {
|
||||
if (protect !== note.isProtected) {
|
||||
const content = note.getContent();
|
||||
|
||||
note.isProtected = protect;
|
||||
note.isProtected = protect;
|
||||
|
||||
// this will force de/encryption
|
||||
note.setContent(content);
|
||||
// this will force de/encryption
|
||||
note.setContent(content);
|
||||
|
||||
note.save();
|
||||
note.save();
|
||||
}
|
||||
|
||||
noteRevisionService.protectNoteRevisions(note);
|
||||
}
|
||||
catch (e) {
|
||||
log.error("Could not un/protect note ID = " + note.noteId);
|
||||
|
||||
noteRevisionService.protectNoteRevisions(note);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function findImageLinks(content, foundLinks) {
|
||||
@@ -668,8 +675,10 @@ function scanForLinks(note) {
|
||||
}
|
||||
}
|
||||
|
||||
function eraseDeletedNotes() {
|
||||
const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
|
||||
function eraseDeletedNotes(eraseNotesAfterTimeInSeconds = null) {
|
||||
if (eraseNotesAfterTimeInSeconds === null) {
|
||||
eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
|
||||
}
|
||||
|
||||
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
|
||||
|
||||
@@ -719,6 +728,10 @@ function eraseDeletedNotes() {
|
||||
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
|
||||
}
|
||||
|
||||
function eraseDeletedNotesNow() {
|
||||
eraseDeletedNotes(0);
|
||||
}
|
||||
|
||||
// do a replace in str - all keys should be replaced by the corresponding values
|
||||
function replaceByMap(str, mapObj) {
|
||||
const re = new RegExp(Object.keys(mapObj).join("|"),"g");
|
||||
@@ -739,7 +752,10 @@ function duplicateSubtree(origNoteId, newParentNoteId) {
|
||||
|
||||
const res = duplicateSubtreeInner(origNote, origBranch, newParentNoteId, noteIdMapping);
|
||||
|
||||
res.note.title += " (dup)";
|
||||
if (!res.note.title.endsWith('(dup)')) {
|
||||
res.note.title += " (dup)";
|
||||
}
|
||||
|
||||
res.note.save();
|
||||
|
||||
return res;
|
||||
@@ -822,9 +838,9 @@ function getNoteIdMapping(origNote) {
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
// first cleanup kickoff 5 minutes after startup
|
||||
setTimeout(cls.wrap(eraseDeletedNotes), 5 * 60 * 1000);
|
||||
setTimeout(cls.wrap(() => eraseDeletedNotes()), 5 * 60 * 1000);
|
||||
|
||||
setInterval(cls.wrap(eraseDeletedNotes), 4 * 3600 * 1000);
|
||||
setInterval(cls.wrap(() => eraseDeletedNotes()), 4 * 3600 * 1000);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
@@ -838,5 +854,6 @@ module.exports = {
|
||||
duplicateSubtree,
|
||||
duplicateSubtreeWithoutRoot,
|
||||
getUndeletedParentBranches,
|
||||
triggerNoteTitleChanged
|
||||
triggerNoteTitleChanged,
|
||||
eraseDeletedNotesNow
|
||||
};
|
||||
|
||||
@@ -31,10 +31,7 @@ function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) {
|
||||
optionService.createOption('openTabs', JSON.stringify([
|
||||
{
|
||||
notePath: startNotePath,
|
||||
active: true,
|
||||
sidebar: {
|
||||
widgets: []
|
||||
}
|
||||
active: true
|
||||
}
|
||||
]), false);
|
||||
|
||||
@@ -103,6 +100,15 @@ function initStartupOptions() {
|
||||
log.info(`Created missing option "${name}" with default value "${value}"`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TRILIUM_START_NOTE_ID || process.env.TRILIUM_SAFE_MODE) {
|
||||
optionService.setOption('openTabs', JSON.stringify([
|
||||
{
|
||||
notePath: process.env.TRILIUM_START_NOTE_ID || 'root',
|
||||
active: true
|
||||
}
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
function getKeyboardDefaultOptions() {
|
||||
|
||||
@@ -43,10 +43,18 @@ function decryptNotes(notes) {
|
||||
}
|
||||
|
||||
function encrypt(plainText) {
|
||||
if (plainText === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataEncryptionService.encrypt(getDataKey(), plainText);
|
||||
}
|
||||
|
||||
function decrypt(cipherText) {
|
||||
if (cipherText === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataEncryptionService.decrypt(getDataKey(), cipherText);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ function runNotesWithLabel(runAttrValue) {
|
||||
}
|
||||
|
||||
sqlInit.dbReady.then(() => {
|
||||
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
|
||||
if (!process.env.TRILIUM_SAFE_MODE) {
|
||||
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
|
||||
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
|
||||
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
|
||||
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const sql = require('./sql');
|
||||
const ScriptContext = require('./script_context');
|
||||
const repository = require('./repository');
|
||||
const cls = require('./cls');
|
||||
|
||||
@@ -32,26 +32,29 @@ class NoteContentProtectedFulltextExp extends Expression {
|
||||
FROM notes JOIN note_contents USING (noteId)
|
||||
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 1`)) {
|
||||
|
||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
content = protectedSessionService.decryptString(content);
|
||||
}
|
||||
catch (e) {
|
||||
log.info('Cannot decrypt content of note', noteId);
|
||||
log.info(`Cannot decrypt content of note ${noteId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
content = content.toLowerCase();
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
content = striptags(content);
|
||||
if (content.length < 20000) { // striptags is slow for very large notes
|
||||
content = striptags(content);
|
||||
}
|
||||
|
||||
content = content.replace(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (this.tokens.find(token => !content.includes(token))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
|
||||
if (!this.tokens.find(token => !content.includes(token))) {
|
||||
resultNoteSet.add(noteCache.notes[noteId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,18 +26,21 @@ class NoteContentUnprotectedFulltextExp extends Expression {
|
||||
FROM notes JOIN note_contents USING (noteId)
|
||||
WHERE type IN ('text', 'code') AND isDeleted = 0 AND isProtected = 0`)) {
|
||||
|
||||
content = content.toString().toLowerCase();
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
content = striptags(content);
|
||||
content = content.replace(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (this.tokens.find(token => !content.includes(token))) {
|
||||
if (!inputNoteSet.hasNoteId(noteId) || !(noteId in noteCache.notes)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
|
||||
content = content.toString().toLowerCase();
|
||||
|
||||
if (type === 'text' && mime === 'text/html') {
|
||||
if (content.length < 20000) { // striptags is slow for very large notes
|
||||
content = striptags(content);
|
||||
}
|
||||
|
||||
content = content.replace(/ /g, ' ');
|
||||
}
|
||||
|
||||
if (!this.tokens.find(token => !content.includes(token))) {
|
||||
resultNoteSet.add(noteCache.notes[noteId]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const Option = require('../entities/option');
|
||||
const TaskContext = require('./task_context.js');
|
||||
const migrationService = require('./migration');
|
||||
const cls = require('./cls');
|
||||
const config = require('./config');
|
||||
|
||||
const dbReady = utils.deferred();
|
||||
|
||||
@@ -131,6 +132,12 @@ function setDbAsInitialized() {
|
||||
}
|
||||
|
||||
dbReady.then(() => {
|
||||
if (config.General && config.General.noBackup === true) {
|
||||
log.info("Disabling scheduled backups.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInterval(() => require('./backup').regularBackup(), 4 * 60 * 60 * 1000);
|
||||
|
||||
// kickoff first backup soon after start up
|
||||
|
||||
@@ -63,8 +63,6 @@
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<link href="libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
|
||||
<!-- Include Fancytree skin and library -->
|
||||
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
|
||||
<script src="libraries/fancytree/jquery.fancytree-all-deps.min.js"></script>
|
||||
|
||||
@@ -127,8 +127,6 @@
|
||||
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script src="libraries/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<link href="libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
|
||||
<script src="app/mobile.js" crossorigin type="module"></script>
|
||||
|
||||
<link href="stylesheets/themes.css" rel="stylesheet">
|
||||
|
||||
Reference in New Issue
Block a user