mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-31 19:51:36 +08:00
Merge branch 'develop' into feat/llm-integration-part2
This commit is contained in:
commit
ba98bd9097
12
.gitattributes
vendored
12
.gitattributes
vendored
@ -1,19 +1,21 @@
|
||||
# Mark files as auto-generated to simplify reviews.
|
||||
package-lock.json linguist-generated=true
|
||||
**/package-lock.json linguist-generated=true
|
||||
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated
|
||||
|
||||
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated=true
|
||||
# Ignore from GitHub language stats.
|
||||
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/**/*.html eol=lf
|
||||
apps/server/src/assets/doc_notes/** linguist-vendored=true
|
||||
apps/edit-docs/demo/** linguist-vendored=true
|
||||
docs/** linguist-vendored=true
|
||||
|
||||
# Normalize line endings.
|
||||
docs/**/*.md eol=lf
|
||||
docs/**/*.json eol=lf
|
||||
|
||||
demo/**/*.html eol=lf
|
||||
demo/**/*.json eol=lf
|
||||
demo/**/*.svg eol=lf
|
||||
demo/**/*.txt eol=lf
|
||||
demo/**/*.js eol=lf
|
||||
demo/**/*.css eol=lf
|
||||
|
||||
*.sh eol=lf
|
||||
|
||||
apps/client/src/libraries/** linguist-vendored
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"templates": {
|
||||
"default": {
|
||||
"includeDate": false
|
||||
}
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@
|
||||
"@playwright/test": "1.52.0",
|
||||
"@stylistic/eslint-plugin": "4.4.0",
|
||||
"@types/express": "5.0.1",
|
||||
"@types/node": "22.15.24",
|
||||
"@types/node": "22.15.29",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.1.4",
|
||||
"eslint": "9.27.0",
|
||||
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/spec-es6" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/libraries" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/libraries" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/docs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/data" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.flatpak-builder" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="@types/jquery" level="application" />
|
||||
</component>
|
||||
</module>
|
@ -274,15 +274,15 @@
|
||||
"revision_last_edited": "此修订版本上次编辑于 {{date}}",
|
||||
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
|
||||
"no_revisions": "此笔记暂无修订版本...",
|
||||
"restore_button": "",
|
||||
"restore_button": "恢复",
|
||||
"confirm_restore": "您是否要恢复此修订版本?这将使用此修订版本覆盖笔记的当前标题和内容。",
|
||||
"delete_button": "",
|
||||
"delete_button": "删除",
|
||||
"confirm_delete": "您是否要删除此修订版本?",
|
||||
"revisions_deleted": "笔记修订版本已删除。",
|
||||
"revision_restored": "笔记修订版本已恢复。",
|
||||
"revision_deleted": "笔记修订版本已删除。",
|
||||
"snapshot_interval": "笔记快照保存间隔: {{seconds}}秒。",
|
||||
"maximum_revisions": "当前笔记的最历史数量: {{number}}。",
|
||||
"maximum_revisions": "当前笔记的最大历史数量: {{number}}。",
|
||||
"settings": "笔记修订设置",
|
||||
"download_button": "下载",
|
||||
"mime": "MIME 类型:",
|
||||
@ -806,7 +806,7 @@
|
||||
"open_full": "展开显示",
|
||||
"collapse": "折叠到正常大小",
|
||||
"title": "笔记地图",
|
||||
"fix-nodes": "修复节点",
|
||||
"fix-nodes": "固定节点",
|
||||
"link-distance": "链接距离"
|
||||
},
|
||||
"note_paths": {
|
||||
@ -1213,7 +1213,7 @@
|
||||
"color": "字体颜色",
|
||||
"bg_color": "背景颜色",
|
||||
"visibility_title": "高亮列表可见性",
|
||||
"visibility_description": "您可以通过添加 #hideHighlightWidget 标签来隐藏每个笔记的高亮小部件。",
|
||||
"visibility_description": "您可以通过添加 #hideHighlightWidget 标签来隐藏单个笔记的高亮小部件。",
|
||||
"shortcut_info": "您可以在选项 -> 快捷键中为快速切换右侧面板(包括高亮列表)配置键盘快捷键(名称为 'toggleRightPane')。"
|
||||
},
|
||||
"table_of_contents": {
|
||||
@ -1547,7 +1547,7 @@
|
||||
"close_other_tabs": "关闭其他标签页",
|
||||
"close_right_tabs": "关闭右侧标签页",
|
||||
"close_all_tabs": "关闭所有标签页",
|
||||
"reopen_last_tab": "重新打开最后一个关闭的标签页",
|
||||
"reopen_last_tab": "重新打开关闭的标签页",
|
||||
"move_tab_to_new_window": "将此标签页移动到新窗口",
|
||||
"copy_tab_to_new_window": "将此标签页复制到新窗口",
|
||||
"new_tab": "新标签页"
|
||||
@ -1616,7 +1616,7 @@
|
||||
"auto-detect-language": "自动检测"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "",
|
||||
"title": "代码块",
|
||||
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
|
||||
"color-scheme": "颜色方案"
|
||||
},
|
||||
@ -1627,21 +1627,21 @@
|
||||
"theme_group_dark": "深色主题"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "格式化"
|
||||
"title": "格式"
|
||||
},
|
||||
"editor": {
|
||||
"title": "编辑器"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "格式化工具栏",
|
||||
"label": "格式工具栏",
|
||||
"floating": {
|
||||
"title": "浮动",
|
||||
"description": "编辑工具出现在光标附近;"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "固定",
|
||||
"description": "编辑工具出现在 \"格式化\" 功能区标签中。"
|
||||
"description": "编辑工具出现在 \"格式\" 功能区标签中。"
|
||||
},
|
||||
"multiline-toolbar": "如果工具栏无法完全显示,则分多行显示。"
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import options from "../../services/options.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
|
||||
export default class CreateAiChatButton extends CommandButtonWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.icon("bx bx-bot")
|
||||
.title(t("ai.create_new_ai_chat"))
|
||||
.titlePlacement("bottom")
|
||||
.command("createAiChat")
|
||||
.class("icon-action");
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return options.get("aiEnabled") === "true";
|
||||
}
|
||||
|
||||
async refreshWithNote() {
|
||||
if (this.isEnabled()) {
|
||||
this.$widget.show();
|
||||
} else {
|
||||
this.$widget.hide();
|
||||
}
|
||||
}
|
||||
}
|
@ -186,7 +186,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
|
||||
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type));
|
||||
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap"].includes(note.type));
|
||||
|
||||
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
|
||||
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));
|
||||
|
@ -188,7 +188,7 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!["text", "code", "render"].includes(this.note?.type ?? "")) {
|
||||
if (!["text", "code", "render", "mindMap"].includes(this.note?.type ?? "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -250,6 +250,8 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
case "text":
|
||||
const readOnly = await this.noteContext?.isReadOnly();
|
||||
return readOnly ? this.htmlHandler : this.textHandler;
|
||||
case "mindMap":
|
||||
return this.htmlHandler;
|
||||
default:
|
||||
console.warn("FindWidget: Unsupported note type for find widget", this.note?.type);
|
||||
}
|
||||
@ -352,7 +354,7 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && ["text", "code", "render"].includes(this.note?.type ?? "");
|
||||
return super.isEnabled() && ["text", "code", "render", "mindMap"].includes(this.note?.type ?? "");
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
|
@ -85,7 +85,7 @@ export default class FindInHtml {
|
||||
if (this.$results?.length) {
|
||||
const $current = this.$results.eq(this.currentIndex);
|
||||
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||
$current[0].scrollIntoView();
|
||||
$current[0].scrollIntoView({ block: 'center', inline: 'center'});
|
||||
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
// Misc note types
|
||||
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
|
||||
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: true },
|
||||
|
||||
// Code notes
|
||||
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },
|
||||
@ -50,7 +49,8 @@ const NOTE_TYPES: NoteTypeMapping[] = [
|
||||
{ type: "image", title: t("note_types.image"), selectable: false },
|
||||
{ type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false },
|
||||
{ type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false },
|
||||
{ type: "search", title: t("note_types.saved-search"), selectable: false }
|
||||
{ type: "search", title: t("note_types.saved-search"), selectable: false },
|
||||
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: false }
|
||||
];
|
||||
|
||||
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);
|
||||
|
@ -378,16 +378,45 @@ export default class TabRowWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
scrollTabContainer(direction: number, behavior: ScrollBehavior = "smooth") {
|
||||
const currentScrollLeft = this.$tabScrollingContainer[0]?.scrollLeft;
|
||||
this.$tabScrollingContainer[0].scrollTo({
|
||||
left: currentScrollLeft + direction,
|
||||
this.$tabScrollingContainer[0].scrollBy({
|
||||
left: direction,
|
||||
behavior
|
||||
});
|
||||
};
|
||||
|
||||
setupScrollEvents() {
|
||||
this.$tabScrollingContainer[0].addEventListener('wheel', (event) => {
|
||||
this.scrollTabContainer(event.deltaY * 1.5);
|
||||
let deltaX = 0;
|
||||
let isScrolling = false;
|
||||
const stepScroll = () => {
|
||||
if (Math.abs(deltaX) > 5) {
|
||||
const step = Math.round(deltaX * 0.2);
|
||||
deltaX -= step;
|
||||
this.scrollTabContainer(step, "instant");
|
||||
requestAnimationFrame(stepScroll);
|
||||
} else {
|
||||
this.scrollTabContainer(deltaX, "instant");
|
||||
deltaX = 0;
|
||||
isScrolling = false;
|
||||
}
|
||||
};
|
||||
this.$tabScrollingContainer[0].addEventListener('wheel', async (event) => {
|
||||
if (!event.shiftKey && event.deltaX === 0) {
|
||||
event.preventDefault();
|
||||
// Clamp deltaX between TAB_CONTAINER_MIN_WIDTH and TAB_CONTAINER_MIN_WIDTH * 3
|
||||
deltaX += Math.sign(event.deltaY) * Math.max(Math.min(Math.abs(event.deltaY), TAB_CONTAINER_MIN_WIDTH * 3), TAB_CONTAINER_MIN_WIDTH);
|
||||
if (!isScrolling) {
|
||||
isScrolling = true;
|
||||
stepScroll();
|
||||
}
|
||||
} else if (event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (event.deltaY > 0) {
|
||||
await appContext.tabManager.activateNextTabCommand();
|
||||
} else {
|
||||
await appContext.tabManager.activatePreviousTabCommand();
|
||||
}
|
||||
this.activeTabEl.scrollIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
this.$scrollButtonLeft[0].addEventListener('click', () => this.scrollTabContainer(-200));
|
||||
|
@ -286,4 +286,13 @@ export default class MindMapWidget extends TypeWidget {
|
||||
utils.downloadSvgAsPng(this.note.title, svg);
|
||||
}
|
||||
|
||||
async executeWithContentElementEvent({ resolve, ntxId }: EventData<"executeWithContentElement">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.initialized;
|
||||
|
||||
resolve(this.$content.find('.main-node-container'));
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,12 @@ async function main() {
|
||||
electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features");
|
||||
electron.app.commandLine.appendSwitch("lang", options.getOptionOrNull("formattingLocale") ?? "en");
|
||||
|
||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||
if (process.platform === "linux") {
|
||||
electron.app.commandLine.appendSwitch("gtk-version", "3");
|
||||
}
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
|
@ -1,5 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import dumpService from "./inc/dump.js";
|
||||
|
@ -115,7 +115,7 @@ test("Search works when dismissing a tab", async ({ page, context }) => {
|
||||
|
||||
await app.getTab(0).click();
|
||||
await app.openAndClickNoteActionMenu("Search in note");
|
||||
await expect(app.findAndReplaceWidget).toBeVisible();
|
||||
await expect(app.findAndReplaceWidget.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("New tab displays workspaces", async ({ page, context }) => {
|
||||
|
@ -87,8 +87,8 @@
|
||||
"mime-types": "3.0.1",
|
||||
"multer": "2.0.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
"ollama": "0.5.15",
|
||||
"openai": "4.103.0",
|
||||
"ollama": "0.5.16",
|
||||
"openai": "4.104.0",
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
|
@ -21,7 +21,7 @@ fi
|
||||
echo "Selected Arch: $ARCH"
|
||||
|
||||
# Set Node.js version and architecture-specific filename
|
||||
NODE_VERSION=22.14.0
|
||||
NODE_VERSION=22.16.0
|
||||
|
||||
script_dir=$(realpath $(dirname $0))
|
||||
BUILD_DIR="$script_dir/../dist"
|
||||
@ -43,7 +43,7 @@ rm -rf $BUILD_DIR/node/lib/node_modules/{npm,corepack} \
|
||||
$BUILD_DIR/node_modules/electron* \
|
||||
$BUILD_DIR/electron*.{js,map}
|
||||
|
||||
printf "#!/bin/sh\n./node/bin/node src/main\n" > $BUILD_DIR/trilium.sh
|
||||
printf "#!/bin/sh\n./node/bin/node main.cjs\n" > $BUILD_DIR/trilium.sh
|
||||
chmod 755 $BUILD_DIR/trilium.sh
|
||||
|
||||
VERSION=`jq -r ".version" package.json`
|
||||
|
@ -1,13 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "blobs" (
|
||||
`blobId` TEXT NOT NULL,
|
||||
`content` TEXT NULL DEFAULT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
PRIMARY KEY(`blobId`)
|
||||
);
|
||||
|
||||
ALTER TABLE notes ADD blobId TEXT DEFAULT NULL;
|
||||
ALTER TABLE note_revisions ADD blobId TEXT DEFAULT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_note_revisions_blobId on note_revisions (blobId);
|
@ -1,4 +0,0 @@
|
||||
DROP TABLE note_contents;
|
||||
DROP TABLE note_revision_contents;
|
||||
|
||||
DELETE FROM entity_changes WHERE entityName IN ('note_contents', 'note_revision_contents');
|
@ -1,26 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "revisions" (`revisionId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT NOT NULL,
|
||||
type TEXT DEFAULT '' NOT NULL,
|
||||
mime TEXT DEFAULT '' NOT NULL,
|
||||
`title` TEXT NOT NULL,
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
blobId TEXT DEFAULT NULL,
|
||||
`utcDateLastEdited` TEXT NOT NULL,
|
||||
`utcDateCreated` TEXT NOT NULL,
|
||||
`utcDateModified` TEXT NOT NULL,
|
||||
`dateLastEdited` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL);
|
||||
|
||||
INSERT INTO revisions (revisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, blobId)
|
||||
SELECT noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, blobId FROM note_revisions;
|
||||
|
||||
DROP TABLE note_revisions;
|
||||
|
||||
CREATE INDEX `IDX_revisions_noteId` ON `revisions` (`noteId`);
|
||||
CREATE INDEX `IDX_revisions_utcDateCreated` ON `revisions` (`utcDateCreated`);
|
||||
CREATE INDEX `IDX_revisions_utcDateLastEdited` ON `revisions` (`utcDateLastEdited`);
|
||||
CREATE INDEX `IDX_revisions_dateCreated` ON `revisions` (`dateCreated`);
|
||||
CREATE INDEX `IDX_revisions_dateLastEdited` ON `revisions` (`dateLastEdited`);
|
||||
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
|
||||
|
||||
UPDATE entity_changes SET entityName = 'revisions' WHERE entityName = 'note_revisions';
|
@ -1,23 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS "attachments"
|
||||
(
|
||||
attachmentId TEXT not null primary key,
|
||||
ownerId TEXT not null,
|
||||
role TEXT not null,
|
||||
mime TEXT not null,
|
||||
title TEXT not null,
|
||||
isProtected INT not null DEFAULT 0,
|
||||
position INT default 0 not null,
|
||||
blobId TEXT DEFAULT null,
|
||||
dateModified TEXT NOT NULL,
|
||||
utcDateModified TEXT not null,
|
||||
utcDateScheduledForErasureSince TEXT DEFAULT NULL,
|
||||
isDeleted INT not null,
|
||||
deleteId TEXT DEFAULT NULL);
|
||||
|
||||
CREATE INDEX IDX_attachments_ownerId_role
|
||||
on attachments (ownerId, role);
|
||||
|
||||
CREATE INDEX IDX_attachments_utcDateScheduledForErasureSince
|
||||
on attachments (utcDateScheduledForErasureSince);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);
|
@ -1,2 +0,0 @@
|
||||
DELETE FROM options WHERE name = 'hideIncludedImages_main';
|
||||
DELETE FROM entity_changes WHERE entityName = 'options' AND entityId = 'hideIncludedImages_main';
|
@ -1,2 +0,0 @@
|
||||
UPDATE options SET name = 'openNoteContexts' WHERE name = 'openTabs';
|
||||
UPDATE entity_changes SET entityId = 'openNoteContexts' WHERE entityName = 'options' AND entityId = 'openTabs';
|
@ -1 +0,0 @@
|
||||
SELECT 1;
|
@ -1,14 +0,0 @@
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'X') WHERE entityName = 'blobs';
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'Y') WHERE entityName = 'blobs';
|
@ -1,3 +0,0 @@
|
||||
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);
|
@ -1 +0,0 @@
|
||||
UPDATE attributes SET value = 'contentAndAttachmentsAndRevisionsSize' WHERE name = 'orderBy' AND value = 'noteSize';
|
@ -1,2 +0,0 @@
|
||||
-- emergency disabling of image compression since it appears to make problems in migration to 0.61
|
||||
UPDATE options SET value = 'false' WHERE name = 'compressImages';
|
@ -1,17 +0,0 @@
|
||||
-- + is normally replaced by X and / by Y, but this can temporarily cause UNIQUE key exception
|
||||
-- this might create blob duplicates, but cleanup will eventually take care of it
|
||||
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'A') WHERE entityName = 'blobs';
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'B') WHERE entityName = 'blobs';
|
@ -1,14 +0,0 @@
|
||||
-- Add the oauth user data table
|
||||
CREATE TABLE IF NOT EXISTS "user_data"
|
||||
(
|
||||
tmpID INT,
|
||||
username TEXT,
|
||||
email TEXT,
|
||||
userIDEncryptedDataKey TEXT,
|
||||
userIDVerificationHash TEXT,
|
||||
salt TEXT,
|
||||
derivedKey TEXT,
|
||||
isSetup TEXT DEFAULT "false",
|
||||
UNIQUE (tmpID),
|
||||
PRIMARY KEY (tmpID)
|
||||
);
|
@ -1,46 +0,0 @@
|
||||
-- Add tables for vector embeddings storage and management
|
||||
-- This migration adds embedding support to the main document.db database
|
||||
|
||||
-- Store embeddings for notes
|
||||
CREATE TABLE IF NOT EXISTS "note_embeddings" (
|
||||
"embedId" TEXT NOT NULL PRIMARY KEY,
|
||||
"noteId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"modelId" TEXT NOT NULL,
|
||||
"dimension" INTEGER NOT NULL,
|
||||
"embedding" BLOB NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"dateCreated" TEXT NOT NULL,
|
||||
"utcDateCreated" TEXT NOT NULL,
|
||||
"dateModified" TEXT NOT NULL,
|
||||
"utcDateModified" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IDX_note_embeddings_noteId" ON "note_embeddings" ("noteId");
|
||||
CREATE INDEX "IDX_note_embeddings_providerId_modelId" ON "note_embeddings" ("providerId", "modelId");
|
||||
|
||||
-- Table to track which notes need embedding updates
|
||||
CREATE TABLE IF NOT EXISTS "embedding_queue" (
|
||||
"noteId" TEXT NOT NULL PRIMARY KEY,
|
||||
"operation" TEXT NOT NULL, -- CREATE, UPDATE, DELETE
|
||||
"dateQueued" TEXT NOT NULL,
|
||||
"utcDateQueued" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastAttempt" TEXT NULL,
|
||||
"error" TEXT NULL,
|
||||
"failed" INTEGER NOT NULL DEFAULT 0,
|
||||
"isProcessing" INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Table to store embedding provider configurations
|
||||
CREATE TABLE IF NOT EXISTS "embedding_providers" (
|
||||
"providerId" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"config" TEXT NOT NULL, -- JSON config object
|
||||
"dateCreated" TEXT NOT NULL,
|
||||
"utcDateCreated" TEXT NOT NULL,
|
||||
"dateModified" TEXT NOT NULL,
|
||||
"utcDateModified" TEXT NOT NULL
|
||||
);
|
@ -1,5 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
@ -3,8 +3,9 @@
|
||||
<h2>Steps</h2>
|
||||
<ul>
|
||||
<li>SSH into your server</li>
|
||||
<li>use <code>wget</code> (or <code>curl</code>) to download latest <code>TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz</code> (notice <code>-Server</code> suffix)
|
||||
on your server.</li>
|
||||
<li>use <code>wget</code> (or <code>curl</code>) to download latest <code>TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz</code> (copy
|
||||
link from <a href="https://github.com/TriliumNext/Notes/releases">release page</a>,
|
||||
notice <code>-Server</code> suffix) on your server.</li>
|
||||
<li>unpack the archive, e.g. using <code>tar -xf -d TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz</code>
|
||||
</li>
|
||||
<li><code>cd trilium-linux-x64-server</code>
|
||||
@ -17,7 +18,9 @@
|
||||
<p>The problem with above steps is that once you close the SSH connection,
|
||||
the Trilium process is terminated. To avoid that, you have two options:</p>
|
||||
<ul>
|
||||
<li>Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: <code>nohup ./trilium &</code>.</li>
|
||||
<li>Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: <code>nohup ./trilium.sh &</code>.
|
||||
(nohup keeps the process running in the background, <code>&</code> runs
|
||||
it in the background)</li>
|
||||
<li>Configure systemd to automatically run Trilium in the background on every
|
||||
boot</li>
|
||||
</ul>
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
<div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container"></div>
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<%- include("./partials/windowGlobal.ejs", locals) %>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import sql from "../../../services/sql.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import sql from "../services/sql.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
interface NoteContentsRow {
|
||||
noteId: string;
|
@ -1,8 +1,8 @@
|
||||
import becca from "../../../becca/becca.js";
|
||||
import becca_loader from "../../../becca/becca_loader.js";
|
||||
import cls from "../../../services/cls.js";
|
||||
import log from "../../../services/log.js";
|
||||
import sql from "../../../services/sql.js";
|
||||
import becca from "../becca/becca.js";
|
||||
import becca_loader from "../becca/becca_loader.js";
|
||||
import cls from "../services/cls.js";
|
||||
import log from "../services/log.js";
|
||||
import sql from "../services/sql.js";
|
||||
|
||||
export default () => {
|
||||
cls.init(() => {
|
295
apps/server/src/migrations/migrations.ts
Normal file
295
apps/server/src/migrations/migrations.ts
Normal file
@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* Contains all the migrations that are run on the database.
|
||||
*/
|
||||
|
||||
// Migrations should be kept in descending order, so the latest migration is first.
|
||||
const MIGRATIONS: (SqlMigration | JsMigration)[] = [
|
||||
// Session store
|
||||
{
|
||||
version: 231,
|
||||
sql: /*sql*/`\
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
||||
`
|
||||
},
|
||||
// Add tables for vector embeddings storage and management
|
||||
// This migration adds embedding support to the main document.db database
|
||||
{
|
||||
version: 230,
|
||||
sql: /*sql*/`\
|
||||
-- Store embeddings for notes
|
||||
CREATE TABLE IF NOT EXISTS "note_embeddings" (
|
||||
"embedId" TEXT NOT NULL PRIMARY KEY,
|
||||
"noteId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"modelId" TEXT NOT NULL,
|
||||
"dimension" INTEGER NOT NULL,
|
||||
"embedding" BLOB NOT NULL,
|
||||
"version" INTEGER NOT NULL DEFAULT 1,
|
||||
"dateCreated" TEXT NOT NULL,
|
||||
"utcDateCreated" TEXT NOT NULL,
|
||||
"dateModified" TEXT NOT NULL,
|
||||
"utcDateModified" TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "IDX_note_embeddings_noteId" ON "note_embeddings" ("noteId");
|
||||
CREATE INDEX "IDX_note_embeddings_providerId_modelId" ON "note_embeddings" ("providerId", "modelId");
|
||||
|
||||
-- Table to track which notes need embedding updates
|
||||
CREATE TABLE IF NOT EXISTS "embedding_queue" (
|
||||
"noteId" TEXT NOT NULL PRIMARY KEY,
|
||||
"operation" TEXT NOT NULL, -- CREATE, UPDATE, DELETE
|
||||
"dateQueued" TEXT NOT NULL,
|
||||
"utcDateQueued" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"attempts" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastAttempt" TEXT NULL,
|
||||
"error" TEXT NULL,
|
||||
"failed" INTEGER NOT NULL DEFAULT 0,
|
||||
"isProcessing" INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Table to store embedding provider configurations
|
||||
CREATE TABLE IF NOT EXISTS "embedding_providers" (
|
||||
"providerId" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 0,
|
||||
"config" TEXT NOT NULL, -- JSON config object
|
||||
"dateCreated" TEXT NOT NULL,
|
||||
"utcDateCreated" TEXT NOT NULL,
|
||||
"dateModified" TEXT NOT NULL,
|
||||
"utcDateModified" TEXT NOT NULL
|
||||
);
|
||||
`
|
||||
},
|
||||
|
||||
// add the oauth user data table
|
||||
{
|
||||
version: 229,
|
||||
sql: /*sql*/`\
|
||||
CREATE TABLE IF NOT EXISTS "user_data"
|
||||
(
|
||||
tmpID INT,
|
||||
username TEXT,
|
||||
email TEXT,
|
||||
userIDEncryptedDataKey TEXT,
|
||||
userIDVerificationHash TEXT,
|
||||
salt TEXT,
|
||||
derivedKey TEXT,
|
||||
isSetup TEXT DEFAULT "false",
|
||||
UNIQUE (tmpID),
|
||||
PRIMARY KEY (tmpID)
|
||||
);
|
||||
`
|
||||
},
|
||||
// fix blob IDs
|
||||
{
|
||||
version: 228,
|
||||
sql: /*sql*/`\
|
||||
-- + is normally replaced by X and / by Y, but this can temporarily cause UNIQUE key exception
|
||||
-- this might create blob duplicates, but cleanup will eventually take care of it
|
||||
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'A');
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'B');
|
||||
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'A') WHERE entityName = 'blobs';
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'B') WHERE entityName = 'blobs';
|
||||
`
|
||||
},
|
||||
// disable image compression
|
||||
{
|
||||
version: 227,
|
||||
sql: /*sql*/`\
|
||||
-- emergency disabling of image compression since it appears to make problems in migration to 0.61
|
||||
UPDATE options SET value = 'false' WHERE name = 'compressImages';
|
||||
`
|
||||
},
|
||||
// rename note size label
|
||||
{
|
||||
version: 226,
|
||||
sql: /*sql*/`\
|
||||
UPDATE attributes SET value = 'contentAndAttachmentsAndRevisionsSize' WHERE name = 'orderBy' AND value = 'noteSize';
|
||||
`
|
||||
},
|
||||
// create blob ID indices
|
||||
{
|
||||
version: 225,
|
||||
sql: /*sql*/`\
|
||||
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);
|
||||
`
|
||||
},
|
||||
// fix blob IDs
|
||||
{
|
||||
version: 224,
|
||||
sql: /*sql*/`\
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE blobs SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE notes SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE attachments SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '+', 'X');
|
||||
UPDATE revisions SET blobId = REPLACE(blobId, '/', 'Y');
|
||||
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '+', 'X') WHERE entityName = 'blobs';
|
||||
UPDATE entity_changes SET entityId = REPLACE(entityId, '/', 'Y') WHERE entityName = 'blobs';
|
||||
`
|
||||
},
|
||||
// no operation
|
||||
{
|
||||
version: 223,
|
||||
sql: /*sql*/`\
|
||||
SELECT 1;
|
||||
`
|
||||
},
|
||||
// rename open tabs to open note contexts
|
||||
{
|
||||
version: 222,
|
||||
sql: /*sql*/`\
|
||||
UPDATE options SET name = 'openNoteContexts' WHERE name = 'openTabs';
|
||||
UPDATE entity_changes SET entityId = 'openNoteContexts' WHERE entityName = 'options' AND entityId = 'openTabs';
|
||||
`
|
||||
},
|
||||
// remove hide included images option
|
||||
{
|
||||
version: 221,
|
||||
sql: /*sql*/`\
|
||||
DELETE FROM options WHERE name = 'hideIncludedImages_main';
|
||||
DELETE FROM entity_changes WHERE entityName = 'options' AND entityId = 'hideIncludedImages_main';
|
||||
`
|
||||
},
|
||||
// migrate images to attachments
|
||||
{
|
||||
version: 220,
|
||||
module: () => import("./0220__migrate_images_to_attachments.js")
|
||||
},
|
||||
// attachments
|
||||
{
|
||||
version: 219,
|
||||
sql: /*sql*/`\
|
||||
CREATE TABLE IF NOT EXISTS "attachments"
|
||||
(
|
||||
attachmentId TEXT not null primary key,
|
||||
ownerId TEXT not null,
|
||||
role TEXT not null,
|
||||
mime TEXT not null,
|
||||
title TEXT not null,
|
||||
isProtected INT not null DEFAULT 0,
|
||||
position INT default 0 not null,
|
||||
blobId TEXT DEFAULT null,
|
||||
dateModified TEXT NOT NULL,
|
||||
utcDateModified TEXT not null,
|
||||
utcDateScheduledForErasureSince TEXT DEFAULT NULL,
|
||||
isDeleted INT not null,
|
||||
deleteId TEXT DEFAULT NULL);
|
||||
|
||||
CREATE INDEX IDX_attachments_ownerId_role
|
||||
on attachments (ownerId, role);
|
||||
|
||||
CREATE INDEX IDX_attachments_utcDateScheduledForErasureSince
|
||||
on attachments (utcDateScheduledForErasureSince);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_attachments_blobId on attachments (blobId);
|
||||
`
|
||||
},
|
||||
// rename note revision to revision
|
||||
{
|
||||
version: 218,
|
||||
sql: /*sql*/`\
|
||||
CREATE TABLE IF NOT EXISTS "revisions" (
|
||||
revisionId TEXT NOT NULL PRIMARY KEY,
|
||||
noteId TEXT NOT NULL,
|
||||
type TEXT DEFAULT '' NOT NULL,
|
||||
mime TEXT DEFAULT '' NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
isProtected INT NOT NULL DEFAULT 0,
|
||||
blobId TEXT DEFAULT NULL,
|
||||
utcDateLastEdited TEXT NOT NULL,
|
||||
utcDateCreated TEXT NOT NULL,
|
||||
utcDateModified TEXT NOT NULL,
|
||||
dateLastEdited TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO revisions (revisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, blobId)
|
||||
SELECT noteRevisionId, noteId, type, mime, title, isProtected, utcDateLastEdited, utcDateCreated, utcDateModified, dateLastEdited, dateCreated, blobId FROM note_revisions;
|
||||
|
||||
DROP TABLE note_revisions;
|
||||
|
||||
CREATE INDEX IDX_revisions_noteId ON revisions (noteId);
|
||||
CREATE INDEX IDX_revisions_utcDateCreated ON revisions (utcDateCreated);
|
||||
CREATE INDEX IDX_revisions_utcDateLastEdited ON revisions (utcDateLastEdited);
|
||||
CREATE INDEX IDX_revisions_dateCreated ON revisions (dateCreated);
|
||||
CREATE INDEX IDX_revisions_dateLastEdited ON revisions (dateLastEdited);
|
||||
CREATE INDEX IF NOT EXISTS IDX_revisions_blobId on revisions (blobId);
|
||||
|
||||
UPDATE entity_changes SET entityName = 'revisions' WHERE entityName = 'note_revisions';
|
||||
`
|
||||
},
|
||||
// drop content tables
|
||||
{
|
||||
version: 217,
|
||||
sql: /*sql*/`\
|
||||
DROP TABLE note_contents;
|
||||
DROP TABLE note_revision_contents;
|
||||
|
||||
DELETE FROM entity_changes WHERE entityName IN ('note_contents', 'note_revision_contents');
|
||||
`
|
||||
},
|
||||
{
|
||||
version: 216,
|
||||
module: async () => import("./0216__move_content_into_blobs.js")
|
||||
},
|
||||
// content structure
|
||||
{
|
||||
version: 215,
|
||||
sql: /*sql*/`\
|
||||
CREATE TABLE IF NOT EXISTS "blobs" (
|
||||
blobId TEXT NOT NULL,
|
||||
content TEXT NULL DEFAULT NULL,
|
||||
dateModified TEXT NOT NULL,
|
||||
utcDateModified TEXT NOT NULL,
|
||||
PRIMARY KEY (blobId)
|
||||
);
|
||||
|
||||
ALTER TABLE notes ADD blobId TEXT DEFAULT NULL;
|
||||
ALTER TABLE note_revisions ADD blobId TEXT DEFAULT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_notes_blobId on notes (blobId);
|
||||
CREATE INDEX IF NOT EXISTS IDX_note_revisions_blobId on note_revisions (blobId);
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
export default MIGRATIONS;
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface SqlMigration extends Migration {
|
||||
sql: string;
|
||||
}
|
||||
|
||||
interface JsMigration extends Migration {
|
||||
module: () => Promise<{ default: () => void }>;
|
||||
}
|
@ -72,7 +72,7 @@ export default function buildLaunchBarConfig() {
|
||||
id: "_lbLlmChat",
|
||||
title: t("hidden-subtree.llm-chat-title"),
|
||||
type: "launcher",
|
||||
command: "createAiChat",
|
||||
builtinWidget: "aiChatLauncher",
|
||||
icon: "bx bx-bot",
|
||||
attributes: [
|
||||
{ type: "label", name: "desktopOnly" }
|
||||
|
@ -1,25 +1,19 @@
|
||||
import backupService from "./backup.js";
|
||||
import sql from "./sql.js";
|
||||
import fs from "fs";
|
||||
import log from "./log.js";
|
||||
import { crash } from "./utils.js";
|
||||
import resourceDir from "./resource_dir.js";
|
||||
import appInfo from "./app_info.js";
|
||||
import cls from "./cls.js";
|
||||
import { t } from "i18next";
|
||||
import { join } from "path";
|
||||
import MIGRATIONS from "../migrations/migrations.js";
|
||||
|
||||
interface MigrationInfo {
|
||||
dbVersion: number;
|
||||
name: string;
|
||||
file: string;
|
||||
type: "sql" | "js" | "ts" | string;
|
||||
/**
|
||||
* Contains the JavaScript/TypeScript migration as a callback method that must be called to trigger the migration.
|
||||
* The method cannot be async since it runs in an SQL transaction.
|
||||
* For SQL migrations, this value is falsy.
|
||||
* If string, then the migration is an SQL script that will be executed.
|
||||
* If a function, then the migration is a JavaScript/TypeScript module that will be executed.
|
||||
*/
|
||||
module?: () => void;
|
||||
migration: string | (() => void);
|
||||
}
|
||||
|
||||
async function migrate() {
|
||||
@ -37,7 +31,6 @@ async function migrate() {
|
||||
);
|
||||
|
||||
const migrations = await prepareMigrations(currentDbVersion);
|
||||
migrations.sort((a, b) => a.dbVersion - b.dbVersion);
|
||||
|
||||
// all migrations are executed in one transaction - upgrade either succeeds, or the user can stay at the old version
|
||||
// otherwise if half of the migrations succeed, user can't use any version - DB is too "new" for the old app,
|
||||
@ -76,53 +69,37 @@ async function migrate() {
|
||||
}
|
||||
|
||||
async function prepareMigrations(currentDbVersion: number): Promise<MigrationInfo[]> {
|
||||
const migrationFiles = fs.readdirSync(resourceDir.MIGRATIONS_DIR) ?? [];
|
||||
MIGRATIONS.sort((a, b) => a.version - b.version);
|
||||
const migrations: MigrationInfo[] = [];
|
||||
for (const file of migrationFiles) {
|
||||
const match = file.match(/^([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js|ts)$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dbVersion = parseInt(match[1]);
|
||||
for (const migration of MIGRATIONS) {
|
||||
const dbVersion = migration.version;
|
||||
if (dbVersion > currentDbVersion) {
|
||||
const name = match[2];
|
||||
const type = match[3];
|
||||
|
||||
const migration: MigrationInfo = {
|
||||
dbVersion: dbVersion,
|
||||
name: name,
|
||||
file: file,
|
||||
type: type
|
||||
};
|
||||
|
||||
if (type === "js" || type === "ts") {
|
||||
if ("sql" in migration) {
|
||||
migrations.push({
|
||||
dbVersion,
|
||||
migration: migration.sql
|
||||
});
|
||||
} else {
|
||||
// Due to ESM imports, the migration file needs to be imported asynchronously and thus cannot be loaded at migration time (since migration is not asynchronous).
|
||||
// As such we have to preload the ESM.
|
||||
// Going back to the original approach but making it webpack-compatible
|
||||
const importPath = join(resourceDir.MIGRATIONS_DIR, file);
|
||||
migration.module = (await import(importPath)).default;
|
||||
migrations.push({
|
||||
dbVersion,
|
||||
migration: (await migration.module()).default
|
||||
});
|
||||
}
|
||||
|
||||
migrations.push(migration);
|
||||
}
|
||||
}
|
||||
return migrations;
|
||||
}
|
||||
|
||||
function executeMigration(mig: MigrationInfo) {
|
||||
if (mig.module) {
|
||||
console.log("Migration with JS module");
|
||||
mig.module();
|
||||
} else if (mig.type === "sql") {
|
||||
const migrationSql = fs.readFileSync(`${resourceDir.MIGRATIONS_DIR}/${mig.file}`).toString("utf8");
|
||||
|
||||
console.log(`Migration with SQL script: ${migrationSql}`);
|
||||
|
||||
sql.executeScript(migrationSql);
|
||||
function executeMigration({ migration }: MigrationInfo) {
|
||||
if (typeof migration === "string") {
|
||||
console.log(`Migration with SQL script: ${migration}`);
|
||||
sql.executeScript(migration);
|
||||
} else {
|
||||
throw new Error(`Unknown migration type '${mig.type}'`);
|
||||
}
|
||||
console.log("Migration with JS module");
|
||||
migration();
|
||||
};
|
||||
}
|
||||
|
||||
function getDbVersion() {
|
||||
|
@ -14,16 +14,8 @@ if (!fs.existsSync(DB_INIT_DIR)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const MIGRATIONS_DIR = path.resolve(DB_INIT_DIR, "migrations");
|
||||
|
||||
if (!fs.existsSync(MIGRATIONS_DIR)) {
|
||||
log.error(`Could not find migration directory: ${MIGRATIONS_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export default {
|
||||
RESOURCE_DIR,
|
||||
MIGRATIONS_DIR,
|
||||
DB_INIT_DIR,
|
||||
ELECTRON_APP_ROOT_DIR
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "fs";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
2
docs/Release Notes/Release Notes/v0.94.0.md
vendored
2
docs/Release Notes/Release Notes/v0.94.0.md
vendored
@ -59,6 +59,7 @@
|
||||
* [Text notes: add a way to move up and down text lines via a keyboard shortcut](https://github.com/TriliumNext/Notes/issues/1002) by @dogfuntom
|
||||
* [improve tab scroll UX by switching from instant to smooth behavior](https://github.com/TriliumNext/Notes/pull/2030) by @SiriusXT
|
||||
* Calendar view: display calendar view if `#viewType=calendar` is set.
|
||||
* [Mind map: add search support](https://github.com/TriliumNext/Notes/pull/2055) by @SiriusXT
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
@ -67,6 +68,7 @@
|
||||
* Add documentation around setting the various environment variables to control upload size limit by @perfectra1n
|
||||
* README improvements by @FliegendeWurst
|
||||
* Improved the documentation of text note keyboard shortcuts (including adding missing shortcuts).
|
||||
* Improvements to "Packaged version for Linux" by @HersheyStormBottle
|
||||
|
||||
## 🌍 Internationalization
|
||||
|
||||
|
@ -4,7 +4,7 @@ This is essentially Trilium sources + node modules + node.js runtime packaged in
|
||||
## Steps
|
||||
|
||||
* SSH into your server
|
||||
* use `wget` (or `curl`) to download latest `TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz` (notice `-Server` suffix) on your server.
|
||||
* use `wget` (or `curl`) to download latest `TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz` (copy link from [release page](https://github.com/TriliumNext/Notes/releases), notice `-Server` suffix) on your server.
|
||||
* unpack the archive, e.g. using `tar -xf -d TriliumNextNotes-Server-[VERSION]-linux-x64.tar.xz`
|
||||
* `cd trilium-linux-x64-server`
|
||||
* `./trilium.sh`
|
||||
@ -12,7 +12,7 @@ This is essentially Trilium sources + node modules + node.js runtime packaged in
|
||||
|
||||
The problem with above steps is that once you close the SSH connection, the Trilium process is terminated. To avoid that, you have two options:
|
||||
|
||||
* Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: `nohup ./trilium &`.
|
||||
* Kill it (with e.g. <kbd>Ctrl</kbd> + <kbd>C</kbd>) and run again like this: `nohup ./trilium.sh &`. (nohup keeps the process running in the background, `&` runs it in the background)
|
||||
* Configure systemd to automatically run Trilium in the background on every boot
|
||||
|
||||
## Configure Trilium to auto-run on boot with systemd
|
||||
|
@ -40,7 +40,7 @@
|
||||
"@playwright/test": "^1.36.0",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "22.15.24",
|
||||
"@types/node": "22.15.29",
|
||||
"@vitest/coverage-v8": "^3.0.5",
|
||||
"@vitest/ui": "^3.0.0",
|
||||
"chalk": "5.4.1",
|
||||
|
@ -30,7 +30,7 @@
|
||||
"@codemirror/lang-xml": "6.1.0",
|
||||
"@codemirror/legacy-modes": "6.5.1",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/view": "6.37.0",
|
||||
"@codemirror/view": "6.37.1",
|
||||
"@fsegurai/codemirror-theme-abcdef": "6.1.4",
|
||||
"@fsegurai/codemirror-theme-abyss": "6.1.4",
|
||||
"@fsegurai/codemirror-theme-android-studio": "6.1.4",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { indentLess, indentMore } from "@codemirror/commands";
|
||||
import { EditorSelection, EditorState, SelectionRange, type ChangeSpec } from "@codemirror/state";
|
||||
import { EditorSelection, EditorState, SelectionRange, type Transaction, type ChangeSpec } from "@codemirror/state";
|
||||
import type { KeyBinding } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
@ -19,57 +19,88 @@ const smartIndentWithTab: KeyBinding[] = [
|
||||
}
|
||||
|
||||
const { selection } = state;
|
||||
const changes: ChangeSpec[] = [];
|
||||
const newSelections: SelectionRange[] = [];
|
||||
|
||||
// Step 1: Handle non-empty selections → replace with tab
|
||||
if (selection.ranges.some(range => !range.empty)) {
|
||||
for (let range of selection.ranges) {
|
||||
changes.push({ from: range.from, to: range.to, insert: "\t" });
|
||||
newSelections.push(EditorSelection.cursor(range.from + 1));
|
||||
// If multiple lines are selected, insert a tab character at the start of each line
|
||||
// and move the cursor to the position after the tab character.
|
||||
const linesCovered = new Set<number>();
|
||||
for (const range of selection.ranges) {
|
||||
const startLine = state.doc.lineAt(range.from);
|
||||
const endLine = state.doc.lineAt(range.to);
|
||||
|
||||
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++) {
|
||||
linesCovered.add(lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
state.update({
|
||||
changes,
|
||||
selection: EditorSelection.create(newSelections),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
})
|
||||
);
|
||||
return true;
|
||||
if (linesCovered.size > 1) {
|
||||
// Multiple lines are selected, indent each line.
|
||||
return indentMore({ state, dispatch });
|
||||
} else {
|
||||
return handleSingleLineSelection(state, dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Handle empty selections
|
||||
for (let range of selection.ranges) {
|
||||
const line = state.doc.lineAt(range.head);
|
||||
const beforeCursor = state.doc.sliceString(line.from, range.head);
|
||||
|
||||
if (/^\s*$/.test(beforeCursor)) {
|
||||
// Only whitespace before cursor → indent line
|
||||
return indentMore({ state, dispatch });
|
||||
} else {
|
||||
// Insert tab character at cursor
|
||||
changes.push({ from: range.head, to: range.head, insert: "\t" });
|
||||
newSelections.push(EditorSelection.cursor(range.head + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length) {
|
||||
dispatch(
|
||||
state.update({
|
||||
changes,
|
||||
selection: EditorSelection.create(newSelections),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return handleEmptySelections(state, dispatch);
|
||||
},
|
||||
shift: indentLess
|
||||
},
|
||||
]
|
||||
export default smartIndentWithTab;
|
||||
|
||||
function handleSingleLineSelection(state: EditorState, dispatch: (transaction: Transaction) => void) {
|
||||
const changes: ChangeSpec[] = [];
|
||||
const newSelections: SelectionRange[] = [];
|
||||
|
||||
// Single line selection, replace with tab.
|
||||
for (let range of state.selection.ranges) {
|
||||
changes.push({ from: range.from, to: range.to, insert: "\t" });
|
||||
newSelections.push(EditorSelection.cursor(range.from + 1));
|
||||
}
|
||||
|
||||
dispatch(
|
||||
state.update({
|
||||
changes,
|
||||
selection: EditorSelection.create(newSelections),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleEmptySelections(state: EditorState, dispatch: (transaction: Transaction) => void) {
|
||||
const changes: ChangeSpec[] = [];
|
||||
const newSelections: SelectionRange[] = [];
|
||||
|
||||
for (let range of state.selection.ranges) {
|
||||
const line = state.doc.lineAt(range.head);
|
||||
const beforeCursor = state.doc.sliceString(line.from, range.head);
|
||||
|
||||
if (/^\s*$/.test(beforeCursor)) {
|
||||
// Only whitespace before cursor → indent line
|
||||
return indentMore({ state, dispatch });
|
||||
} else {
|
||||
// Insert tab character at cursor
|
||||
changes.push({ from: range.head, to: range.head, insert: "\t" });
|
||||
newSelections.push(EditorSelection.cursor(range.head + 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (changes.length) {
|
||||
dispatch(
|
||||
state.update({
|
||||
changes,
|
||||
selection: EditorSelection.create(newSelections),
|
||||
scrollIntoView: true,
|
||||
userEvent: "input"
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { Range, RangeSet } from "@codemirror/state";
|
||||
import { foldState, unfoldEffect } from "@codemirror/language";
|
||||
import { Range, RangeSet, StateEffect } from "@codemirror/state";
|
||||
|
||||
const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" });
|
||||
const activeMatchDecoration = Decoration.mark({ class: "cm-activeMatch" });
|
||||
@ -16,6 +17,7 @@ export class SearchHighlighter {
|
||||
currentFound: number;
|
||||
totalFound: number;
|
||||
matcher?: MatchDecorator;
|
||||
searchRegexp?: RegExp;
|
||||
private parsedMatches: Match[];
|
||||
|
||||
constructor(public view: EditorView) {
|
||||
@ -42,6 +44,7 @@ export class SearchHighlighter {
|
||||
regexp: regex,
|
||||
decoration: searchMatchDecoration,
|
||||
});
|
||||
this.searchRegexp = regex;
|
||||
this.#updateSearchData(this.view);
|
||||
this.#scrollToMatchNearestSelection();
|
||||
}
|
||||
@ -77,8 +80,23 @@ export class SearchHighlighter {
|
||||
const match = this.parsedMatches[matchIndex];
|
||||
this.currentFound = matchIndex + 1;
|
||||
this.activeMatch = activeMatchDecoration.range(match.from, match.to);
|
||||
|
||||
// Check if the match is inside a folded region.
|
||||
const unfoldEffects: StateEffect<unknown>[] = [];
|
||||
const folded = this.view.state.field(foldState);
|
||||
const iter = folded.iter();
|
||||
while (iter.value) {
|
||||
if (match.from >= iter.from && match.to <= iter.to) {
|
||||
unfoldEffects.push(unfoldEffect.of({ from: iter.from, to: iter.to }));
|
||||
}
|
||||
iter.next();
|
||||
}
|
||||
|
||||
this.view.dispatch({
|
||||
effects: EditorView.scrollIntoView(match.from, { y: "center" }),
|
||||
effects: [
|
||||
...unfoldEffects,
|
||||
EditorView.scrollIntoView(match.from, { y: "center" })
|
||||
],
|
||||
scrollIntoView: true
|
||||
});
|
||||
}
|
||||
@ -98,17 +116,21 @@ export class SearchHighlighter {
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = this.matcher.createDeco(view);
|
||||
const cursor = matches.iter();
|
||||
while (cursor.value) {
|
||||
this.parsedMatches.push({
|
||||
from: cursor.from,
|
||||
to: cursor.to
|
||||
});
|
||||
cursor.next();
|
||||
// Create the match decorator which will automatically highlight matches in the document.
|
||||
this.matches = this.matcher.createDeco(view);
|
||||
|
||||
// Manually search for matches in the current document in order to get the total number of matches.
|
||||
const parsedMatches: Match[] = [];
|
||||
const text = view.state.doc.toString();
|
||||
let match: RegExpExecArray | null | undefined;
|
||||
while ((match = this.searchRegexp?.exec(text))) {
|
||||
const from = match.index ?? 0;
|
||||
const to = from + match[0].length;
|
||||
|
||||
parsedMatches.push({ from, to });
|
||||
}
|
||||
|
||||
this.matches = matches;
|
||||
this.parsedMatches = parsedMatches;
|
||||
this.totalFound = this.parsedMatches.length;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
|
||||
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter } from "@codemirror/language";
|
||||
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter, codeFolding } from "@codemirror/language";
|
||||
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { vim } from "@replit/codemirror-vim";
|
||||
@ -73,6 +73,7 @@ export default class CodeMirror extends EditorView {
|
||||
]),
|
||||
highlightSelectionMatches(),
|
||||
bracketMatching(),
|
||||
codeFolding(),
|
||||
foldGutter(),
|
||||
indentationMarkers(),
|
||||
];
|
||||
|
493
pnpm-lock.yaml
generated
493
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user