Compare commits

..

16 Commits

Author SHA1 Message Date
perf3ct
10988095c2 feat(search): get the correct comparison and rice out the fts5 search 2025-10-27 14:37:44 -07:00
perf3ct
253da139de feat(search): try again to get fts5 searching done well 2025-10-24 21:47:06 -07:00
Jon Fuller
d992a5e4a2 Merge branch 'main' into feat/rice-searching-with-sqlite 2025-10-24 09:18:11 -07:00
perf3ct
58c225237c feat(search): try a ground-up sqlite search approach 2025-09-03 00:34:55 +00:00
perf3ct
d074841885 Revert "feat(search): try to get fts search to work in large environments"
This reverts commit 053f722cb8.
2025-09-02 19:24:50 +00:00
perf3ct
06b2d71b27 Revert "feat(search): try to decrease complexity"
This reverts commit 5b79e0d71e.
2025-09-02 19:24:47 +00:00
perf3ct
0afb8a11c8 Revert "feat(search): try to deal with huge dbs, might need to squash later"
This reverts commit 37d0136c50.
2025-09-02 19:24:46 +00:00
perf3ct
f529ddc601 Revert "feat(search): further improve fts search"
This reverts commit 7c5553bd4b.
2025-09-02 19:24:45 +00:00
perf3ct
8572f82e0a Revert "feat(search): I honestly have no idea what I'm doing"
This reverts commit b09a2c386d.
2025-09-02 19:24:44 +00:00
perf3ct
b09a2c386d feat(search): I honestly have no idea what I'm doing 2025-09-01 22:29:59 -07:00
perf3ct
7c5553bd4b feat(search): further improve fts search 2025-09-01 21:40:05 -07:00
perf3ct
37d0136c50 feat(search): try to deal with huge dbs, might need to squash later 2025-09-01 04:33:10 +00:00
perf3ct
5b79e0d71e feat(search): try to decrease complexity 2025-08-30 22:30:01 -07:00
perf3ct
053f722cb8 feat(search): try to get fts search to work in large environments 2025-08-31 03:15:29 +00:00
perf3ct
21aaec2c38 feat(search): also fix tests for new fts functionality 2025-08-30 20:48:42 +00:00
perf3ct
1db4971da6 feat(search): implement FST5 w/ sqlite for faster and better searching
feat(search): don't limit the number of blobs to put in virtual tables

fix(search): improve FTS triggers to handle all SQL operations correctly

The root cause of FTS index issues during import was that database triggers
weren't properly handling all SQL operations, particularly upsert operations
(INSERT ... ON CONFLICT ... DO UPDATE) that are commonly used during imports.

Key improvements:
- Fixed INSERT trigger to handle INSERT OR REPLACE operations
- Updated UPDATE trigger to fire on ANY change (not just specific columns)
- Improved blob triggers to use INSERT OR REPLACE for atomic updates
- Added proper handling for notes created before their blobs (import scenario)
- Added triggers for protection state changes
- All triggers now use LEFT JOIN to handle missing blobs gracefully

This ensures the FTS index stays synchronized even when:
- Entity events are disabled during import
- Notes are re-imported (upsert operations)
- Blobs are deduplicated across notes
- Notes are created before their content blobs

The solution works entirely at the database level through triggers,
removing the need for application-level workarounds.

fix(search): consolidate FTS trigger fixes into migration 234

- Merged improved trigger logic from migration 235 into 234
- Deleted unnecessary migration 235 since DB version is still 234
- Ensures triggers handle all SQL operations (INSERT OR REPLACE, upserts)
- Fixes FTS indexing for imported notes by handling missing blobs
- Schema.sql and migration 234 now have identical trigger implementations
2025-08-30 20:39:40 +00:00
575 changed files with 10002 additions and 11932 deletions

View File

@@ -12,7 +12,7 @@ runs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: "pnpm"
- name: Install dependencies
shell: bash

View File

@@ -1,4 +1,6 @@
name: Deploy Documentation
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
# This workflow builds and deploys your MkDocs site when changes are pushed to main
name: Deploy MkDocs Documentation
on:
# Trigger on push to main branch
@@ -9,8 +11,11 @@ on:
# Only run when docs files change
paths:
- 'docs/**'
- 'apps/edit-docs/**'
- 'packages/share-theme/**'
- 'README.md' # README is synced to docs/index.md
- 'mkdocs.yml'
- 'requirements-docs.txt'
- '.github/workflows/deploy-docs.yml'
- 'scripts/fix-mkdocs-structure.ts'
# Allow manual triggering from Actions tab
workflow_dispatch:
@@ -22,12 +27,15 @@ on:
- master
paths:
- 'docs/**'
- 'apps/edit-docs/**'
- 'packages/share-theme/**'
- 'README.md' # README is synced to docs/index.md
- 'mkdocs.yml'
- 'requirements-docs.txt'
- '.github/workflows/deploy-docs.yml'
- 'scripts/fix-mkdocs-structure.ts'
jobs:
build-and-deploy:
name: Build and Deploy Documentation
name: Build and Deploy MkDocs
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -41,25 +49,72 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
cache: 'pip'
cache-dependency-path: 'requirements-docs.txt'
- name: Install MkDocs and Dependencies
run: |
pip install --upgrade pip
pip install -r requirements-docs.txt
env:
PIP_DISABLE_PIP_VERSION_CHECK: 1
# Setup pnpm before fixing docs structure
- name: Setup pnpm
uses: pnpm/action-setup@v4
# Setup Node.js with pnpm
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
node-version: '22'
cache: 'pnpm'
# Install Node.js dependencies for the TypeScript script
- name: Install Dependencies
run: pnpm install --frozen-lockfile
run: |
pnpm install --frozen-lockfile
- name: Trigger build of documentation
run: pnpm docs:build
- name: Fix Documentation Structure
run: |
# Fix duplicate navigation entries by moving overview pages to index.md
pnpm run chore:fix-mkdocs-structure
- name: Build MkDocs Site
run: |
# Build with strict mode but allow expected warnings
mkdocs build --verbose || {
EXIT_CODE=$?
# Check if the only issue is expected warnings
if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
[ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
echo "✅ Build succeeded with expected warnings"
mkdocs build --verbose
else
echo "❌ Build failed with unexpected errors"
exit $EXIT_CODE
fi
}
- name: Fix HTML Links
run: |
# Remove .md extensions from links in generated HTML
pnpm tsx ./scripts/fix-html-links.ts site
- name: Validate Built Site
run: |
# Basic validation that important files exist
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
echo "✅ Site validation passed"
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages

View File

@@ -30,7 +30,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: "pnpm"
- run: pnpm install --frozen-lockfile

View File

@@ -46,7 +46,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: "pnpm"
- name: Install npm dependencies
@@ -86,12 +86,12 @@ jobs:
- name: Upload Playwright trace
if: failure()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output
- uses: actions/upload-artifact@v5
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: Playwright report (${{ matrix.dockerfile }})
@@ -116,10 +116,10 @@ jobs:
- dockerfile: Dockerfile
platform: linux/arm64
image: ubuntu-24.04-arm
- dockerfile: Dockerfile.legacy
- dockerfile: Dockerfile
platform: linux/arm/v7
image: ubuntu-24.04-arm
- dockerfile: Dockerfile.legacy
- dockerfile: Dockerfile
platform: linux/arm/v8
image: ubuntu-24.04-arm
runs-on: ${{ matrix.image }}
@@ -146,7 +146,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: 'pnpm'
- name: Install dependencies
@@ -209,7 +209,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
path: /tmp/digests/*
@@ -223,7 +223,7 @@ jobs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
path: /tmp/digests
pattern: digests-*

View File

@@ -52,7 +52,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -89,7 +89,7 @@ jobs:
name: Nightly Build
- name: Publish artifacts
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
if: ${{ github.event_name == 'pull_request' }}
with:
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}

View File

@@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: 'pnpm'
- name: Install dependencies
@@ -35,7 +35,7 @@ jobs:
- name: Upload test report
if: failure()
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: e2e report
path: apps/server-e2e/test-output

View File

@@ -50,7 +50,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -73,7 +73,7 @@ jobs:
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Upload the artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
path: apps/desktop/upload/*.*
@@ -100,7 +100,7 @@ jobs:
arch: ${{ matrix.arch }}
- name: Upload the artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: release-server-linux-${{ matrix.arch }}
path: upload/*.*
@@ -120,7 +120,7 @@ jobs:
docs/Release Notes
- name: Download all artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v5
with:
merge-multiple: true
pattern: release-*

View File

@@ -30,7 +30,7 @@ jobs:
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
node-version: 22
cache: "pnpm"
- name: Install dependencies

2
.nvmrc
View File

@@ -1 +1 @@
24.11.0
22.21.0

View File

@@ -37,18 +37,20 @@
"devDependencies": {
"@playwright/test": "1.56.1",
"@stylistic/eslint-plugin": "5.5.0",
"@types/express": "5.0.5",
"@types/node": "24.9.2",
"@types/yargs": "17.0.34",
"@types/express": "5.0.3",
"@types/node": "22.18.12",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4",
"eslint": "9.39.0",
"eslint": "9.38.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.5",
"lorem-ipsum": "2.0.8",
"rcedit": "4.0.1",
"rimraf": "6.1.0",
"tslib": "2.8.1"
"rimraf": "6.0.1",
"tslib": "2.8.1",
"typedoc": "0.28.14",
"typedoc-plugin-missing-exports": "4.1.2"
},
"optionalDependencies": {
"appdmg": "0.6.6"

15
_regroup/typedoc.json Normal file
View File

@@ -0,0 +1,15 @@
{
"entryPoints": [
"src/services/backend_script_entrypoint.ts",
"src/public/app/services/frontend_script_entrypoint.ts"
],
"plugin": [
"typedoc-plugin-missing-exports"
],
"outputs": [
{
"name": "html",
"path": "./docs/Script API"
}
]
}

View File

@@ -1,22 +0,0 @@
{
"name": "build-docs",
"version": "1.0.0",
"description": "",
"main": "src/main.ts",
"scripts": {
"start": "tsx ."
},
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.19.0",
"devDependencies": {
"@redocly/cli": "2.10.0",
"archiver": "7.0.1",
"fs-extra": "11.3.2",
"react": "19.2.0",
"react-dom": "19.2.0",
"typedoc": "0.28.14",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -1,36 +0,0 @@
/**
* The backend script API is accessible to code notes with the "JS (backend)" language.
*
* The entire API is exposed as a single global: {@link api}
*
* @module Backend Script API
*/
/**
* This file creates the entrypoint for TypeDoc that simulates the context from within a
* script note on the server side.
*
* Make sure to keep in line with backend's `script_context.ts`.
*/
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
export type { BNote };
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
import BNote from "../../server/src/becca/entities/bnote.js";
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
export type { Api };
const fakeNote = new BNote();
/**
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
*/
export const api: Api = new BackendScriptApi(fakeNote, {});

View File

@@ -1,127 +0,0 @@
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
process.env.NODE_ENV = "development";
import cls from "@triliumnext/server/src/services/cls.js";
import { dirname, join, resolve } from "path";
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra";
import archiver from "archiver";
import { WriteStream } from "fs";
import { execSync } from "child_process";
import BuildContext from "./context.js";
const DOCS_ROOT = "../../../docs";
const OUTPUT_DIR = "../../site";
async function buildDocsInner() {
const i18n = await import("@triliumnext/server/src/services/i18n.js");
await i18n.initializeTranslations();
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
await sqlInit.createInitialDatabase(true);
const note = await importData(join(__dirname, DOCS_ROOT, "User Guide"));
// Export
const zipFilePath = "output.zip";
try {
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
const branch = note.getParentBranches()[0];
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
"no-progress-reporting",
"export",
null
);
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
await exportToZip(taskContext, branch, "share", fileOutputStream);
await waitForStreamToFinish(fileOutputStream);
await extractZip(zipFilePath, OUTPUT_DIR);
} finally {
if (await fsExtra.exists(zipFilePath)) {
await fsExtra.rm(zipFilePath);
}
}
// Copy favicon.
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
console.log("Documentation built successfully!");
}
export async function importData(path: string) {
const buffer = await createImportZip(path);
const importService = (await import("@triliumnext/server/src/services/import/zip.js")).default;
const TaskContext = (await import("@triliumnext/server/src/services/task_context.js")).default;
const context = new TaskContext("no-progress-reporting", "importNotes", null);
const becca = (await import("@triliumnext/server/src/becca/becca.js")).default;
const rootNote = becca.getRoot();
if (!rootNote) {
throw new Error("Missing root note for import.");
}
return await importService.importZip(context, buffer, rootNote, {
preserveIds: true
});
}
async function createImportZip(path: string) {
const inputFile = "input.zip";
const archive = archiver("zip", {
zlib: { level: 0 }
});
console.log("Archive path is ", resolve(path))
archive.directory(path, "/");
const outputStream = fsExtra.createWriteStream(inputFile);
archive.pipe(outputStream);
archive.finalize();
await waitForStreamToFinish(outputStream);
try {
return await fsExtra.readFile(inputFile);
} finally {
await fsExtra.rm(inputFile);
}
}
function waitForStreamToFinish(stream: WriteStream) {
return new Promise<void>((res, rej) => {
stream.on("finish", () => res());
stream.on("error", (err) => rej(err));
});
}
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
// We ignore directories since they can appear out of order anyway.
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
const destPath = join(outputPath, entry.fileName);
const fileContent = await readContent(zip, entry);
await fsExtra.mkdirs(dirname(destPath));
await fs.writeFile(destPath, fileContent);
}
zip.readEntry();
});
}
export default async function buildDocs({ gitRootDir }: BuildContext) {
// Build the share theme.
execSync(`pnpm run --filter share-theme build`, {
stdio: "inherit",
cwd: gitRootDir
});
// Trigger the actual build.
await new Promise((res, rej) => {
cls.init(() => {
buildDocsInner()
.catch(rej)
.then(res);
});
});
}

View File

@@ -1,4 +0,0 @@
export default interface BuildContext {
gitRootDir: string;
baseDir: string;
}

View File

@@ -1,28 +0,0 @@
/**
* The front script API is accessible to code notes with the "JS (frontend)" language.
*
* The entire API is exposed as a single global: {@link api}
*
* @module Frontend Script API
*/
/**
* This file creates the entrypoint for TypeDoc that simulates the context from within a
* script note.
*
* Make sure to keep in line with frontend's `script_context.ts`.
*/
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
export type { default as FNote } from "../../client/src/entities/fnote.js";
export type { Api } from "../../client/src/services/frontend_script_api.js";
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
//@ts-expect-error
export const api: Api = new FrontendScriptApi();

View File

@@ -1,26 +0,0 @@
import { join } from "path";
import BuildContext from "./context";
import buildSwagger from "./swagger";
import { existsSync, mkdirSync, rmSync } from "fs";
import buildDocs from "./build-docs";
import buildScriptApi from "./script-api";
const context: BuildContext = {
gitRootDir: join(__dirname, "../../../"),
baseDir: join(__dirname, "../../../site")
};
async function main() {
// Clean input dir.
if (existsSync(context.baseDir)) {
rmSync(context.baseDir, { recursive: true });
}
mkdirSync(context.baseDir);
// Start building.
await buildDocs(context);
buildSwagger(context);
buildScriptApi(context);
}
main();

View File

@@ -1,15 +0,0 @@
import { execSync } from "child_process";
import BuildContext from "./context";
import { join } from "path";
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
// Generate types
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
for (const config of [ "backend", "frontend" ]) {
const outDir = join(baseDir, "script-api", config);
execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
stdio: "inherit"
});
}
}

View File

@@ -1,32 +0,0 @@
import BuildContext from "./context";
import { join } from "path";
import { execSync } from "child_process";
import { mkdirSync } from "fs";
interface BuildInfo {
specPath: string;
outDir: string;
}
const DIR_PREFIX = "rest-api";
const buildInfos: BuildInfo[] = [
{
// Paths are relative to Git root.
specPath: "apps/server/internal.openapi.yaml",
outDir: `${DIR_PREFIX}/internal`
},
{
specPath: "apps/server/etapi.openapi.yaml",
outDir: `${DIR_PREFIX}/etapi`
}
];
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
for (const { specPath, outDir } of buildInfos) {
const absSpecPath = join(gitRootDir, specPath);
const targetDir = join(baseDir, outDir);
mkdirSync(targetDir, { recursive: true });
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
}
}

View File

@@ -1,36 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2020",
"outDir": "dist",
"strict": false,
"types": [
"node",
"express"
],
"rootDir": "src",
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
},
"include": [
"src/**/*.ts",
"../server/src/*.d.ts"
],
"exclude": [
"eslint.config.js",
"eslint.config.cjs",
"eslint.config.mjs"
],
"references": [
{
"path": "../server/tsconfig.app.json"
},
{
"path": "../desktop/tsconfig.app.json"
},
{
"path": "../client/tsconfig.app.json"
}
]
}

View File

@@ -1,15 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": [],
"references": [
{
"path": "../server"
},
{
"path": "../client"
},
{
"path": "./tsconfig.app.json"
}
]
}

View File

@@ -1,10 +0,0 @@
{
"$schema": "https://typedoc.org/schema.json",
"name": "Trilium Backend API",
"entryPoints": [
"src/backend_script_entrypoint.ts"
],
"plugin": [
"typedoc-plugin-missing-exports"
]
}

View File

@@ -1,10 +0,0 @@
{
"$schema": "https://typedoc.org/schema.json",
"name": "Trilium Frontend API",
"entryPoints": [
"src/frontend_script_entrypoint.ts"
],
"plugin": [
"typedoc-plugin-missing-exports"
]
}

View File

@@ -15,7 +15,7 @@
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
},
"dependencies": {
"@eslint/js": "9.39.0",
"@eslint/js": "9.38.0",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
@@ -37,12 +37,12 @@
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"color": "5.0.2",
"dayjs": "1.11.19",
"dayjs": "1.11.18",
"dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"globals": "16.4.0",
"i18next": "25.6.0",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
@@ -54,12 +54,12 @@
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "16.4.1",
"mermaid": "11.12.1",
"mind-elixir": "5.3.4",
"mermaid": "11.12.0",
"mind-elixir": "5.3.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.27.2",
"react-i18next": "16.2.3",
"react-i18next": "16.1.2",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -74,9 +74,9 @@
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.1",
"@types/tabulator-tables": "6.3.0",
"@types/tabulator-tables": "6.2.11",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.0.10",
"happy-dom": "20.0.7",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4"
}

View File

@@ -218,12 +218,12 @@ export type CommandMappings = {
/** Works only in the electron context menu. */
replaceMisspelling: CommandData;
importMarkdownInline: CommandData;
showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string };
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
showPasteMarkdownDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData;
@@ -270,7 +270,6 @@ export type CommandMappings = {
closeThisNoteSplit: CommandData;
moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
jumpToNote: CommandData;
openTodayNote: CommandData;
commandPalette: CommandData;
// Keyboard shortcuts

View File

@@ -159,16 +159,6 @@ export default class Entrypoints extends Component {
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
}
async openTodayNoteCommand() {
const todayNote = await dateNoteService.getTodayNote();
if (!todayNote) {
console.warn("Missing today note.");
return;
}
await appContext.tabManager.openInSameTab(todayNote.noteId);
}
async runActiveNoteCommand() {
const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) {

View File

@@ -417,7 +417,7 @@ export default class FNote {
return notePaths;
}
getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root";
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
@@ -428,23 +428,7 @@ export default class FNote {
isHidden: path.includes("_hidden")
}));
// Calculate the length of the prefix match between two arrays
const prefixMatchLength = (path: string[], target: string[]) => {
const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
};
notePaths.sort((a, b) => {
if (activeNotePath) {
const activeSegments = activeNotePath.split('/');
const aOverlap = prefixMatchLength(a.notePath, activeSegments);
const bOverlap = prefixMatchLength(b.notePath, activeSegments);
// Paths with more matching prefix segments are prioritized
// when the match count is equal, other criteria are used for sorting
if (bOverlap !== aOverlap) {
return bOverlap - aOverlap;
}
}
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isArchived !== b.isArchived) {
@@ -465,11 +449,10 @@ export default class FNote {
* Returns the note path considered to be the "best"
*
* @param {string} [hoistedNoteId='root']
* @param {string|null} [activeNotePath=null]
* @return {string[]} array of noteIds constituting the particular note path
*/
getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
getBestNotePath(hoistedNoteId = "root") {
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
}
/**

View File

@@ -56,20 +56,7 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
}
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
const container = containerRef.current!;
container.replaceChildren(...$renderedContent);
// Wait for all images to load.
const images = Array.from(container.querySelectorAll("img"));
await Promise.all(
images.map(img => {
if (img.complete) return Promise.resolve();
return new Promise<void>(resolve => {
img.addEventListener("load", () => resolve(), { once: true });
img.addEventListener("error", () => resolve(), { once: true });
});
})
);
containerRef.current?.replaceChildren(...$renderedContent);
}
load().then(() => requestAnimationFrame(onReady))

View File

@@ -0,0 +1,28 @@
/**
* The front script API is accessible to code notes with the "JS (frontend)" language.
*
* The entire API is exposed as a single global: {@link api}
*
* @module Frontend Script API
*/
/**
* This file creates the entrypoint for TypeDoc that simulates the context from within a
* script note.
*
* Make sure to keep in line with frontend's `script_context.ts`.
*/
export type { default as BasicWidget } from "../widgets/basic_widget.js";
export type { default as FAttachment } from "../entities/fattachment.js";
export type { default as FAttribute } from "../entities/fattribute.js";
export type { default as FBranch } from "../entities/fbranch.js";
export type { default as FNote } from "../entities/fnote.js";
export type { Api } from "./frontend_script_api.js";
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
//@ts-expect-error
export const api: Api = new FrontendScriptApi();

View File

@@ -20,6 +20,9 @@ function setupGlobs() {
window.glob.froca = froca;
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
// for CKEditor integration (button on block toolbar)
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = String(msg).toLowerCase();

View File

@@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
file: null,
image: null,
launcher: null,
mermaid: "s1aBHPd79XYj",
mermaid: null,
mindMap: null,
noteMap: null,
relationMap: null,

View File

@@ -26,12 +26,21 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
}
const path = notePath.split("/").reverse();
if (!path.includes("root")) {
path.push("root");
}
const effectivePathSegments: string[] = [];
let childNoteId: string | null = null;
let i = 0;
for (let i = 0; i < path.length; i++) {
const parentNoteId = path[i];
while (true) {
if (i >= path.length) {
break;
}
const parentNoteId = path[i++];
if (childNoteId !== null) {
const child = await froca.getNote(childNoteId, !logErrors);
@@ -56,7 +65,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
return null;
}
if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
if (!parents.some((p) => p.noteId === parentNoteId)) {
if (logErrors) {
const parent = froca.getNoteFromCache(parentNoteId);
@@ -68,8 +77,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
);
}
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
const bestNotePath = child.getBestNotePath(hoistedNoteId);
if (bestNotePath) {
const pathToRoot = bestNotePath.reverse().slice(1);
@@ -100,9 +108,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
if (!note) {
throw new Error(`Unable to find note: ${notePath}.`);
}
const activeNotePath = appContext.tabManager.getActiveContextNotePath();
const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
const bestNotePath = note.getBestNotePath(hoistedNoteId);
if (!bestNotePath) {
throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);

View File

@@ -11,11 +11,7 @@ export function reloadFrontendApp(reason?: string) {
logInfo(`Frontend app reload: ${reason}`);
}
if (isElectron()) {
dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
} else {
window.location.reload();
}
window.location.reload();
}
export function restartDesktopApp() {

84
apps/client/src/share.ts Normal file
View File

@@ -0,0 +1,84 @@
import "normalize.css";
import "boxicons/css/boxicons.min.css";
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
import "@triliumnext/share-theme/styles/index.css";
import "@triliumnext/share-theme/scripts/index.js";
async function ensureJQuery() {
const $ = (await import("jquery")).default;
(window as any).$ = $;
}
async function applyMath() {
const anyMathBlock = document.querySelector("#content .math-tex");
if (!anyMathBlock) {
return;
}
const renderMathInElement = (await import("./services/math.js")).renderMathInElement;
renderMathInElement(document.getElementById("content"));
}
async function formatCodeBlocks() {
const anyCodeBlock = document.querySelector("#content pre");
if (!anyCodeBlock) {
return;
}
await ensureJQuery();
const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
await formatCodeBlocks($("#content"));
}
async function setupTextNote() {
formatCodeBlocks();
applyMath();
const setupMermaid = (await import("./share/mermaid.js")).default;
setupMermaid();
}
/**
* Fetch note with given ID from backend
*
* @param noteId of the given note to be fetched. If false, fetches current note.
*/
async function fetchNote(noteId: string | null = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
const resp = await fetch(`api/notes/${noteId}`);
return await resp.json();
}
document.addEventListener(
"DOMContentLoaded",
() => {
const noteType = determineNoteType();
if (noteType === "text") {
setupTextNote();
}
const toggleMenuButton = document.getElementById("toggleMenuButton");
const layout = document.getElementById("layout");
if (toggleMenuButton && layout) {
toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
}
},
false
);
function determineNoteType() {
const bodyClass = document.body.className;
const match = bodyClass.match(/type-([^\s]+)/);
return match ? match[1] : null;
}
// workaround to prevent webpack from removing "fetchNote" as dead code:
// add fetchNote as property to the window object
Object.defineProperty(window, "fetchNote", {
value: fetchNote
});

View File

@@ -1,12 +1,7 @@
export default async function setupMermaid() {
const mermaidEls = document.querySelectorAll("#content pre code.language-mermaid");
if (mermaidEls.length === 0) {
return;
}
import mermaid from "mermaid";
const mermaid = (await import("mermaid")).default;
for (const codeBlock of mermaidEls) {
export default function setupMermaid() {
for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
const parentPre = codeBlock.parentElement;
if (!parentPre) {
continue;

View File

@@ -2034,9 +2034,9 @@ body.zen #right-pane,
body.zen #mobile-sidebar-wrapper,
body.zen .tab-row-container,
body.zen .tab-row-widget,
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)),
body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
body.zen .note-icon-widget,
body.zen .title-row .icon-action,
body.zen .floating-buttons-children > *:not(.bx-edit-alt),

View File

@@ -12,9 +12,6 @@
"toast": {
"critical-error": {
"title": "خطأ فادح"
},
"widget-error": {
"title": "فشل في البدء بعنصر الواجهة"
}
},
"add_link": {
@@ -29,8 +26,7 @@
"edit_branch_prefix": "تعديل بادئة الفرع",
"prefix": "البادئة: ",
"save": "حفظ",
"help_on_tree_prefix": "مساعدة حول بادئة الشجرة",
"branch_prefix_saved": "تم حفظ بادئة الفرع."
"help_on_tree_prefix": "مساعدة حول بادئة الشجرة"
},
"bulk_actions": {
"bulk_actions": "اجراءات جماعية",
@@ -87,8 +83,7 @@
"workspace_calendar_root": "‎تحديد جذر التقويم لكل مساحة عمل",
"hide_highlight_widget": "اخفاء عنصر واجهة قائمة التمييزات",
"is_owned_by_note": "تخص الملاحظة",
"and_more": "... و {{count}}مرات اكثر.",
"related_notes_title": "ملاحظات اخرى بنفس التسمية"
"and_more": "... و {{count}}مرات اكثر."
},
"rename_label": {
"to": "الى",
@@ -132,9 +127,7 @@
"delete_attachment": "حذف المرفق",
"upload_new_revision": "رفع مراجعة جديدة",
"copy_link_to_clipboard": "نسخ الرابط الى الحافظة",
"convert_attachment_into_note": "تحويل المرفق الى ملاحظة",
"delete_success": "تم حذف المرفق \"{{title}}\" .",
"enter_new_name": "ادخل اسم مرفق جديد"
"convert_attachment_into_note": "تحويل المرفق الى ملاحظة"
},
"calendar": {
"week": "أسبوع",
@@ -266,8 +259,7 @@
"note_paths": {
"search": "بحث",
"archived": "مؤرشف",
"title": "مسارات الملاحظة",
"clone_button": "جار نسخ الملاحظة الى مكان جديد..."
"title": "مسارات الملاحظة"
},
"script_executor": {
"query": "استعلام",
@@ -380,8 +372,7 @@
"export_note_title": "تصدير الملاحظة",
"export_status": "حالة التصدير",
"export_finished_successfully": "اكتمل التصدير بنجاح.",
"export_in_progress": "جار التصدير: {{progressCount}}",
"choose_export_type": "اختر نوع التصدير اولا من فضلك"
"export_in_progress": "جار التصدير: {{progressCount}}"
},
"help": {
"troubleshooting": "أستكشاف الاخطاء واصلاحها",
@@ -411,10 +402,7 @@
"movingCloningNotes": "نقل/ استنساخ الملاحظات",
"deleteNotes": "حذف الملاحظة/ الشجرة الفرعية",
"collapseWholeTree": "طي شجرة الملاحظة باكملها",
"followLink": "اتبع تلرابط تحت المؤشر",
"onlyInDesktop": "في سطح المكتب فقط(Electron build)",
"createEditLink": "انشاء/ تحرير رابط خارجي",
"quickSearch": "الانتقال الى مربع البحث السريع"
"followLink": "اتبع تلرابط تحت المؤشر"
},
"import": {
"options": "خيارات",
@@ -477,13 +465,7 @@
"delete_all_button": "حذف كل المراجعات",
"settings": "اعدادات مراجعة الملاحظة",
"diff_not_available": "المقارنة غير متوفرة.",
"help_title": "مساعدة حول مراجعات الملاحظة",
"diff_off_hint": "انقر لعرض محتويات الملاحظة",
"revisions_deleted": "تم حذف جميع نسخ المراجعات للملاحظة.",
"revision_restored": "تم استعادة نسخ المراجعة للملاحظة.",
"revision_deleted": "تم حذف مراجعة الملاحظة.",
"snapshot_interval": "فاصل زمني لحفظ لقطات اصدارات المراجعة: {{seconds}}",
"maximum_revisions": "حد عدد لقطات اصدارات الملاحظة: {{number}}"
"help_title": "مساعدة حول مراجعات الملاحظة"
},
"sort_child_notes": {
"title": "عنوان",
@@ -497,15 +479,13 @@
"sorting_direction": "اتجاه الترتيب",
"natural_sort": "الترتيب الطبيعي",
"natural_sort_language": "لغات الترتيب الطبيعي",
"sort_children_by": "ترتيب العناصر الفرعية حسب...",
"sort_folders_at_top": "ترتيب المجلدات في الاعلى"
"sort_children_by": "ترتيب العناصر الفرعية حسب..."
},
"recent_changes": {
"undelete_link": "الغاء الحذف",
"title": "التغيرات الاخيرة",
"no_changes_message": "لايوجد تغيير لحد الان...",
"erase_notes_button": "مسح الملاحظات المحذوفة الان",
"deleted_notes_message": "تم حذف الملاحظات نهائيا."
"erase_notes_button": "مسح الملاحظات المحذوفة الان"
},
"edited_notes": {
"deleted": "(حذف)",
@@ -716,6 +696,7 @@
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
},
"etapi": {
"wiki": "ويكي",
"created": "تم الأنشاء",
"actions": "أجراءات",
"title": "ETAPI",
@@ -724,9 +705,7 @@
"default_token_name": "رمز جديد",
"rename_token_title": "اعادة تسمية الرمز",
"rename_token": "اعادة تسمية هذا الرمز",
"create_token": "انشاء رمز PEAPI جديد",
"new_token_title": "رمز ETAPI جديد",
"token_created_title": "انشاء رمز ETAPI"
"create_token": "انشاء رمز PEAPI جديد"
},
"password": {
"heading": "كلمة المرور",
@@ -832,8 +811,7 @@
"help_on_links": "مساعدة حول الارتباطات التشعبية",
"notes_to_clone": "ملاحظات للنسخ",
"target_parent_note": "الملاحظة الاصلية الهدف",
"clone_to_selected_note": "استنساخ الى الملاحظة المحددة",
"no_path_to_clone_to": "لايوجد مسار لنسخ المحتوى الية."
"clone_to_selected_note": "استنساخ الى الملاحظة المحددة"
},
"table_of_contents": {
"unit": "عناوين",
@@ -1051,8 +1029,7 @@
},
"delete_note": {
"delete_note": "حذف الملاحظة",
"delete_matched_notes": "حف الملاحظات المطابقة",
"delete_matched_notes_description": "سوف يؤدي هذا الى حذف الملاحظات المطابقة."
"delete_matched_notes": "حف الملاحظات المطابقة"
},
"rename_note": {
"rename_note": "اعادة تسمية الملاحظة",
@@ -1335,8 +1312,7 @@
"notes_to_move": "الملاحظات المراد نقلها",
"target_parent_note": "ملاحظة الاصل الهدف",
"dialog_title": "انقل الملاحظات الى...",
"move_button": "نقل الىالملاحظة المحددة",
"error_no_path": "لايوجد مسار لنقل العنصر الية."
"move_button": "نقل الىالملاحظة المحددة"
},
"delete_revisions": {
"delete_note_revisions": "حذف مراجعات الملاحظة"
@@ -1387,8 +1363,7 @@
"save_attributes": "حفظ السمات <enter>",
"add_a_new_attribute": "اضافة سمة جديدة",
"add_new_label_definition": "اضافة تعريف لتسمية جديدة",
"add_new_relation_definition": "اضافة تعريف لعلاقة جديدة",
"add_new_relation": "اضافة علاقة جديدة <kbd data-command=\"addNewRelation\">"
"add_new_relation_definition": "اضافة تعريف لعلاقة جديدة"
},
"zen_mode": {
"button_exit": "الخروج من وضع Zen"
@@ -1459,8 +1434,5 @@
},
"png_export_button": {
"button_title": "تصدير المخطط كملف PNG"
},
"protected_session_status": {
"inactive": "انقر للدخول الى جلسة محمية"
}
}

View File

@@ -51,7 +51,7 @@
"bulk_actions_executed": "批量操作已成功执行。",
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
"labels": "标签",
"relations": "关系",
"relations": "关联关系",
"notes": "笔记",
"other": "其它"
},
@@ -104,8 +104,7 @@
"export_status": "导出状态",
"export_in_progress": "导出进行中:{{progressCount}}",
"export_finished_successfully": "导出成功完成。",
"format_pdf": "PDF - 用于打印或共享目的。",
"share-format": "HTML 网页发布——采用与共享笔记相同的主题,但可发布为静态网站。"
"format_pdf": "PDF - 用于打印或共享目的。"
},
"help": {
"noteNavigation": "笔记导航",
@@ -185,8 +184,7 @@
},
"import-status": "导入状态",
"in-progress": "导入进行中:{{progress}}",
"successful": "导入成功完成。",
"importZipRecommendation": "导入 ZIP 文件时,笔记层级将反映压缩文件内的子目录结构。"
"successful": "导入成功完成。"
},
"include_note": {
"dialog_title": "包含笔记",
@@ -261,6 +259,7 @@
"delete_all_revisions": "删除此笔记的所有修订版本",
"delete_all_button": "删除所有修订版本",
"help_title": "关于笔记修订版本的帮助",
"revision_last_edited": "此修订版本上次编辑于 {{date}}",
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
"no_revisions": "此笔记暂无修订版本...",
"restore_button": "恢复",
@@ -1289,6 +1288,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI 是一个 REST API用于以编程方式访问 Trilium 实例,而无需 UI。",
"see_more": "有关更多详细信息,请参见 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
"wiki": "维基",
"openapi_spec": "ETAPI OpenAPI 规范",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "创建新的 ETAPI 令牌",
"existing_tokens": "现有令牌",
"no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
@@ -1555,9 +1558,7 @@
"window-on-top": "保持此窗口置顶"
},
"note_detail": {
"could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget",
"printing": "正在打印…",
"printing_pdf": "正在导出为PDF…"
"could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget"
},
"note_title": {
"placeholder": "请输入笔记标题..."

View File

@@ -4,7 +4,7 @@
"homepage": "Startseite:",
"app_version": "App-Version:",
"db_version": "DB-Version:",
"sync_version": "Sync-Version:",
"sync_version": "Synch-version:",
"build_date": "Build-Datum:",
"build_revision": "Build-Revision:",
"data_directory": "Datenverzeichnis:"
@@ -104,8 +104,7 @@
"export_status": "Exportstatus",
"export_in_progress": "Export läuft: {{progressCount}}",
"export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
"format_pdf": "PDF - für Ausdrucke oder Teilen.",
"share-format": "HTML für die Web-Veröffentlichung verwendet dasselbe Theme wie bei freigegebenen Notizen, kann jedoch als statische Website veröffentlicht werden."
"format_pdf": "PDF - für Ausdrucke oder Teilen."
},
"help": {
"noteNavigation": "Notiz Navigation",
@@ -185,8 +184,7 @@
},
"import-status": "Importstatus",
"in-progress": "Import läuft: {{progress}}",
"successful": "Import erfolgreich abgeschlossen.",
"importZipRecommendation": "Beim Import einer ZIP-Datei wird die Notizhierarchie aus der Ordnerstruktur im Archiv übernommen."
"successful": "Import erfolgreich abgeschlossen."
},
"include_note": {
"dialog_title": "Notiz beifügen",
@@ -261,6 +259,7 @@
"delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
"delete_all_button": "Alle Revisionen löschen",
"help_title": "Hilfe zu Notizrevisionen",
"revision_last_edited": "Diese Revision wurde zuletzt am {{date}} bearbeitet",
"confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
"no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
"confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
@@ -648,8 +647,7 @@
"logout": "Abmelden",
"show-cheatsheet": "Cheatsheet anzeigen",
"toggle-zen-mode": "Zen Modus",
"new-version-available": "Neues Update verfügbar",
"download-update": "Version {{latestVersion}} herunterladen"
"new-version-available": "Neues Update verfügbar"
},
"sync_status": {
"unknown": "<p>Der Synchronisations-Status wird bekannt, sobald der nächste Synchronisierungsversuch gestartet wird.</p><p>Klicke, um eine Synchronisierung jetzt auszulösen.</p>",
@@ -991,7 +989,7 @@
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
"start_session_button": "Starte eine geschützte Sitzung <kbd>Eingabetaste</kbd>",
"started": "Geschützte Sitzung gestartet.",
"wrong_password": "Passwort falsch.",
"wrong_password": "Passwort flasch.",
"protecting-finished-successfully": "Geschützt erfolgreich beendet.",
"unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.",
"protecting-in-progress": "Schützen läuft: {{count}}",
@@ -1286,6 +1284,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
"see_more": "Weitere Details können im {{- link_to_wiki}} und in der {{- link_to_openapi_spec}} oder der {{- link_to_swagger_ui }} gefunden werden.",
"wiki": "Wiki",
"openapi_spec": "ETAPI OpenAPI-Spezifikation",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Erstelle ein neues ETAPI-Token",
"existing_tokens": "Vorhandene Token",
"no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
@@ -1519,9 +1521,7 @@
"window-on-top": "Dieses Fenster immer oben halten"
},
"note_detail": {
"could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden",
"printing": "Druckvorgang läuft…",
"printing_pdf": "PDF-Export läuft…"
"could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden"
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…"
@@ -1654,7 +1654,7 @@
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
"cut": "Ausschneiden",
"copy": "Kopieren",
"copy-link": "Link kopieren",
"copy-link": "Link opieren",
"paste": "Einfügen",
"paste-as-plain-text": "Als unformatierten Text einfügen",
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"
@@ -2079,7 +2079,6 @@
},
"presentation_view": {
"edit-slide": "Folie bearbeiten",
"start-presentation": "Präsentation starten",
"slide-overview": "Übersicht der Folien ein-/ausblenden"
"start-presentation": "Präsentation starten"
}
}

View File

@@ -104,8 +104,7 @@
"export_status": "Export status",
"export_in_progress": "Export in progress: {{progressCount}}",
"export_finished_successfully": "Export finished successfully.",
"format_pdf": "PDF - for printing or sharing purposes.",
"share-format": "HTML for web publishing - uses the same theme that is used shared notes, but can be published as a static website."
"format_pdf": "PDF - for printing or sharing purposes."
},
"help": {
"title": "Cheatsheet",
@@ -261,6 +260,7 @@
"delete_all_revisions": "Delete all revisions of this note",
"delete_all_button": "Delete all revisions",
"help_title": "Help on Note Revisions",
"revision_last_edited": "This revision was last edited on {{date}}",
"confirm_delete_all": "Do you want to delete all revisions of this note?",
"no_revisions": "No revisions for this note yet...",
"restore_button": "Restore",
@@ -1453,6 +1453,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.",
"see_more": "See more details in the {{- link_to_wiki}} and the {{- link_to_openapi_spec}} or the {{- link_to_swagger_ui }}.",
"wiki": "wiki",
"openapi_spec": "ETAPI OpenAPI spec",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Create new ETAPI token",
"existing_tokens": "Existing tokens",
"no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",

View File

@@ -104,8 +104,7 @@
"export_status": "Estado de exportación",
"export_in_progress": "Exportación en curso: {{progressCount}}",
"export_finished_successfully": "La exportación finalizó exitosamente.",
"format_pdf": "PDF - para propósitos de impresión o compartición.",
"share-format": "HTML para publicación web: utiliza el mismo tema que se utiliza en las notas compartidas, pero se puede publicar como un sitio web estático."
"format_pdf": "PDF - para propósitos de impresión o compartición."
},
"help": {
"noteNavigation": "Navegación de notas",
@@ -185,8 +184,7 @@
},
"import-status": "Estado de importación",
"in-progress": "Importación en progreso: {{progress}}",
"successful": "Importación finalizada exitosamente.",
"importZipRecommendation": "Al importar un archivo ZIP, la jerarquía de notas reflejará la estructura de subdirectorios dentro del archivo comprimido."
"successful": "Importación finalizada exitosamente."
},
"include_note": {
"dialog_title": "Incluir nota",
@@ -261,6 +259,7 @@
"delete_all_revisions": "Eliminar todas las revisiones de esta nota",
"delete_all_button": "Eliminar todas las revisiones",
"help_title": "Ayuda sobre revisiones de notas",
"revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
"no_revisions": "Aún no hay revisiones para esta nota...",
"restore_button": "Restaurar",
@@ -1446,6 +1445,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI es una REST API que se utiliza para acceder a la instancia de Trilium mediante programación, sin interfaz de usuario.",
"see_more": "Véa más detalles en el {{- link_to_wiki}} y el {{- link_to_openapi_spec}} o el {{- link_to_swagger_ui }}.",
"wiki": "wiki",
"openapi_spec": "Especificación ETAPI OpenAPI",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Crear nuevo token ETAPI",
"existing_tokens": "Tokens existentes",
"no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
@@ -1712,9 +1715,7 @@
"window-on-top": "Mantener esta ventana en la parte superior"
},
"note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
"printing": "Impresión en curso...",
"printing_pdf": "Exportando a PDF en curso.."
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"
},
"note_title": {
"placeholder": "escriba el título de la nota aquí..."

View File

@@ -260,6 +260,7 @@
"delete_all_revisions": "Supprimer toutes les versions de cette note",
"delete_all_button": "Supprimer toutes les versions",
"help_title": "Aide sur les versions de notes",
"revision_last_edited": "Cette version a été modifiée pour la dernière fois le {{date}}",
"confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
"no_revisions": "Aucune version pour cette note pour l'instant...",
"confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
@@ -1288,6 +1289,8 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.",
"wiki": "wiki",
"openapi_spec": "Spec ETAPI OpenAPI",
"create_token": "Créer un nouveau jeton ETAPI",
"existing_tokens": "Jetons existants",
"no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
@@ -1304,7 +1307,9 @@
"delete_token": "Supprimer/désactiver ce token",
"rename_token_title": "Renommer le jeton",
"rename_token_message": "Veuillez saisir le nom du nouveau jeton",
"delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?"
"delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?",
"see_more": "Voir plus de détails dans le {{- link_to_wiki}} et le {{- link_to_openapi_spec}} ou le {{- link_to_swagger_ui }}.",
"swagger_ui": "Interface utilisateur ETAPI Swagger"
},
"options_widget": {
"options_status": "Statut des options",

View File

@@ -1,5 +0,0 @@
{
"about": {
"title": "ट्रिलियम नोट्स के बारें में"
}
}

View File

@@ -1,50 +1 @@
{
"about": {
"title": "A Trilium Notes-ról",
"homepage": "Kezdőlap:",
"app_version": "Alkalmazás verziója:",
"db_version": "Adatbázis verzió:",
"sync_version": "Verzió szinkronizálás :",
"build_revision": "Build revízió:",
"data_directory": "Adatkönyvtár:",
"build_date": "Build dátum:"
},
"toast": {
"critical-error": {
"title": "Kritikus hiba",
"message": "Kritikus hiba történt, amely megakadályozza a kliensalkalmazás indítását:\n\n{{message}}\n\nEzt valószínűleg egy váratlan szkripthiba okozza. Próbálja meg biztonságos módban elindítani az alkalmazást, és hárítsa el a problémát."
},
"widget-error": {
"title": "Nem sikerült inicializálni egy widgetet",
"message-custom": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó egyéni widget inicializálása sikertelen volt a következő ok miatt:\n\n{{message}}",
"message-unknown": "Ismeretlen widget inicializálása sikertelen volt a következő ok miatt:\n\n{{message}}"
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
}
},
"add_link": {
"add_link": "Link hozzáadása",
"help_on_links": "Segítség a linkekhez",
"note": "Jegyzet",
"search_note": "név szerinti jegyzetkeresés",
"link_title_mirrors": "A link cím tükrözi a jegyzet aktuális címét",
"link_title_arbitrary": "link cím önkényesen módosítható",
"link_title": "Link cím",
"button_add_link": "Link hozzáadása"
},
"branch_prefix": {
"edit_branch_prefix": "Az elágazás előtagjának szerkesztése",
"help_on_tree_prefix": "Segítség a fa előtagján",
"prefix": "Az előtag: ",
"save": "Mentés"
},
"bulk_actions": {
"bulk_actions": "Tömeges akciók",
"affected_notes": "Érintett jegyzetek",
"labels": "Címkék",
"relations": "Kapcsolatok",
"notes": "Jegyzetek"
}
}
{}

View File

@@ -132,6 +132,10 @@
"new_token_message": "Inserisci il nome del nuovo token",
"title": "ETAPI",
"description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.",
"see_more": "Per maggiori dettagli consulta {{- link_to_wiki}} e {{- link_to_openapi_spec}} o {{- link_to_swagger_ui}}.",
"wiki": "wiki",
"openapi_spec": "Specifiche ETAPI OpenAPI",
"swagger_ui": "Interfaccia utente ETAPI Swagger",
"create_token": "Crea un nuovo token ETAPI",
"existing_tokens": "Token esistenti",
"no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
@@ -863,6 +867,7 @@
"delete_all_revisions": "Elimina tutte le revisioni di questa nota",
"delete_all_button": "Elimina tutte le revisioni",
"help_title": "Aiuto sulle revisioni delle note",
"revision_last_edited": "Questa revisione è stata modificata l'ultima volta il {{date}}",
"confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
"no_revisions": "Ancora nessuna revisione per questa nota...",
"restore_button": "Ripristina",

View File

@@ -254,8 +254,7 @@
"export_status": "エクスポート状況",
"export_in_progress": "エクスポート処理中: {{progressCount}}",
"export_finished_successfully": "エクスポートが正常に完了しました。",
"format_pdf": "PDF - 印刷または共有目的に。",
"share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
"format_pdf": "PDF - 印刷または共有目的に。"
},
"help": {
"title": "チートシート",
@@ -611,6 +610,7 @@
"delete_all_revisions": "このノートの変更履歴をすべて削除",
"delete_all_button": "変更履歴をすべて削除",
"help_title": "変更履歴のヘルプ",
"revision_last_edited": "この変更は{{date}}に行われました",
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
"no_revisions": "このノートに変更履歴はまだありません...",
"restore_button": "復元",
@@ -657,6 +657,10 @@
"created": "作成日時",
"title": "ETAPI",
"description": "ETAPI は、Trilium インスタンスに UI なしでプログラム的にアクセスするための REST API です。",
"see_more": "詳細は{{- link_to_wiki}}と{{- link_to_openapi_spec}}または{{- link_to_swagger_ui }}を参照してください。",
"wiki": "wiki",
"openapi_spec": "ETAPI OpenAPIの仕様",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "新しくETAPIトークンを作成",
"existing_tokens": "既存のトークン",
"no_tokens_yet": "トークンはまだありません。上のボタンをクリックして作成してください。",

View File

@@ -13,13 +13,6 @@
"critical-error": {
"title": "Kritische Error",
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
},
"widget-error": {
"title": "Starten widget mislukt",
"message-unknown": "Onbekende widget kan niet gestart worden omdat:\n\n{{message}}"
},
"bundle-error": {
"title": "Custom script laden mislukt"
}
},
"add_link": {

View File

@@ -912,6 +912,7 @@
"delete_all_revisions": "Usuń wszystkie wersje tej notatki",
"delete_all_button": "Usuń wszystkie wersje",
"help_title": "Pomoc dotycząca wersji notatki",
"revision_last_edited": "Ta wersja była ostatnio edytowana {{date}}",
"confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
"no_revisions": "Brak wersji dla tej notatki...",
"restore_button": "Przywróć",
@@ -1663,6 +1664,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.",
"see_more": "Zobacz więcej szczegółów w {{- link_to_wiki}} oraz w {{- link_to_openapi_spec}} lub {{- link_to_swagger_ui }}.",
"wiki": "wiki",
"openapi_spec": "specyfikacja ETAPI OpenAPI",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Utwórz nowy token ETAPI",
"existing_tokens": "Istniejące tokeny",
"no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",

View File

@@ -259,6 +259,7 @@
"delete_all_revisions": "Apagar todas as versões desta nota",
"delete_all_button": "Apagar todas as versões",
"help_title": "Ajuda sobre as versões da nota",
"revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
"confirm_delete_all": "Quer apagar todas as versões desta nota?",
"no_revisions": "Ainda não há versões para esta nota...",
"restore_button": "Recuperar",
@@ -1422,6 +1423,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.",
"see_more": "Veja mais pormenores no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
"wiki": "wiki",
"openapi_spec": "Especificação OpenAPI do ETAPI",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Criar token ETAPI",
"existing_tokens": "Tokens existentes",
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",

View File

@@ -415,6 +415,7 @@
"delete_all_revisions": "Excluir todas as versões desta nota",
"delete_all_button": "Excluir todas as versões",
"help_title": "Ajuda sobre as versões da nota",
"revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
"confirm_delete_all": "Você quer excluir todas as versões desta nota?",
"no_revisions": "Ainda não há versões para esta nota...",
"restore_button": "Recuperar",
@@ -1932,6 +1933,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
"see_more": "Veja mais detalhes no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
"wiki": "wiki",
"openapi_spec": "Especificação OpenAPI do ETAPI",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Criar novo token ETAPI",
"existing_tokens": "Tokens existentes",
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",

View File

@@ -507,13 +507,17 @@
"new_token_message": "Introduceți denumirea noului token",
"new_token_title": "Token ETAPI nou",
"no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.",
"openapi_spec": "Specificația OpenAPI pentru ETAPI",
"swagger_ui": "UI-ul Swagger pentru ETAPI",
"rename_token": "Redenumește token-ul",
"rename_token_message": "Introduceți denumirea noului token",
"rename_token_title": "Redenumire token",
"see_more": "Vedeți mai multe detalii în {{- link_to_wiki}} și în {{- link_to_openapi_spec}} sau în {{- link_to_swagger_ui }}.",
"title": "ETAPI",
"token_created_message": "Copiați token-ul creat în clipboard. Trilium stochează token-ul ca hash așadar această valoare poate fi văzută doar acum.",
"token_created_title": "Token ETAPI creat",
"token_name": "Denumire token"
"token_name": "Denumire token",
"wiki": "wiki"
},
"execute_script": {
"example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
@@ -1086,6 +1090,7 @@
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
"restore_button": "Restaurează",
"revision_deleted": "Revizia notiței a fost ștearsă.",
"revision_last_edited": "Revizia a fost ultima oară modificată pe {{date}}",
"revision_restored": "Revizia notiței a fost restaurată.",
"revisions_deleted": "Notița reviziei a fost ștearsă.",
"maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",

View File

@@ -320,8 +320,7 @@
"explodeArchivesTooltip": "Если этот флажок установлен, Trilium будет читать файлы <code>.zip</code>, <code>.enex</code> и <code>.opml</code> и создавать заметки из файлов внутри этих архивов. Если флажок не установлен, Trilium будет прикреплять сами архивы к заметке.",
"explodeArchives": "Прочитать содержимое архивов <code>.zip</code>, <code>.enex</code> и <code>.opml</code>.",
"shrinkImagesTooltip": "<p>Если этот параметр включен, Trilium попытается уменьшить размер импортируемых изображений путём масштабирования и оптимизации, что может повлиять на воспринимаемое качество изображения. Если этот параметр не установлен, изображения будут импортированы без изменений.</p><p>Это не относится к импорту файлов <code>.zip</code> с метаданными, поскольку предполагается, что эти файлы уже оптимизированы.</p>",
"codeImportedAsCode": "Импортировать распознанные файлы кода (например, <code>.json</code>) в виде заметок типа \"код\", если это неясно из метаданных",
"importZipRecommendation": "При импорте ZIP файла иерархия заметок будет отражена в структуре папок внутри архива."
"codeImportedAsCode": "Импортировать распознанные файлы кода (например, <code>.json</code>) в виде заметок типа \"код\", если это неясно из метаданных"
},
"markdown_import": {
"dialog_title": "Импорт Markdown",
@@ -366,6 +365,7 @@
"delete_all_button": "Удалить все версии",
"help_title": "Помощь по версиям заметок",
"confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
"revision_last_edited": "Эта версия последний раз редактировалась {{date}}",
"confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
"confirm_delete": "Вы хотите удалить эту версию?",
"revisions_deleted": "Версии заметки были удалены.",
@@ -980,8 +980,7 @@
"open_sql_console_history": "Открыть историю консоли SQL",
"show_shared_notes_subtree": "Поддерево общедоступных заметок",
"switch_to_mobile_version": "Перейти на мобильную версию",
"switch_to_desktop_version": "Переключиться на версию для ПК",
"new-version-available": "Доступно обновление"
"switch_to_desktop_version": "Переключиться на версию для ПК"
},
"zpetne_odkazy": {
"backlink": "{{count}} ссылки",
@@ -1440,6 +1439,7 @@
},
"etapi": {
"title": "ETAPI",
"wiki": "вики",
"created": "Создано",
"actions": "Действия",
"existing_tokens": "Существующие токены",
@@ -1447,7 +1447,10 @@
"default_token_name": "новый токен",
"rename_token_title": "Переименовать токен",
"description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
"see_more": "Более подробную информацию смотрите в {{- link_to_wiki}} и {{- link_to_openapi_spec}} или {{- link_to_swagger_ui }}.",
"create_token": "Создать новый токен ETAPI",
"openapi_spec": "Спецификация ETAPI OpenAPI",
"swagger_ui": "Пользовательский интерфейс ETAPI Swagger",
"new_token_title": "Новый токен ETAPI",
"token_created_title": "Создан токен ETAPI",
"rename_token": "Переименовать этот токен",

View File

@@ -256,6 +256,7 @@
"delete_all_revisions": "Obriši sve revizije ove beleške",
"delete_all_button": "Obriši sve revizije",
"help_title": "Pomoć za Revizije beleški",
"revision_last_edited": "Ova revizija je poslednji put izmenjena {{date}}",
"confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
"no_revisions": "Još uvek nema revizija za ovu belešku...",
"restore_button": "Vrati",

View File

@@ -104,8 +104,7 @@
"export_in_progress": "正在匯出:{{progressCount}}",
"export_finished_successfully": "成功匯出。",
"format_html": "HTML - 推薦,因為它保留了所有格式",
"format_pdf": "PDF - 用於列印或與他人分享。",
"share-format": "HTML 網頁發佈——使用與共享筆記相同的佈景主題,但可發佈為靜態網站。"
"format_pdf": "PDF - 用於列印或與他人分享。"
},
"help": {
"noteNavigation": "筆記導航",
@@ -261,6 +260,7 @@
"delete_all_revisions": "刪除此筆記的所有歷史版本",
"delete_all_button": "刪除所有歷史版本",
"help_title": "關於筆記歷史版本的說明",
"revision_last_edited": "此歷史版本上次於 {{date}} 編輯",
"confirm_delete_all": "您是否要刪除此筆記的所有歷史版本?",
"no_revisions": "此筆記暫無歷史版本…",
"confirm_restore": "您是否要還原此歷史版本?這將使用此歷史版本覆寫筆記的目前標題和內容。",
@@ -1281,6 +1281,8 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI 是一個 REST API用於以編程方式訪問 Trilium 實例,而無需 UI。",
"wiki": "維基",
"openapi_spec": "ETAPI OpenAPI 規範",
"create_token": "新增 ETAPI 令牌",
"existing_tokens": "現有令牌",
"no_tokens_yet": "目前還沒有令牌。點擊上面的按鈕新增一個。",
@@ -1297,7 +1299,9 @@
"delete_token": "刪除 / 停用此令牌",
"rename_token_title": "重新命名令牌",
"rename_token_message": "請輸入新的令牌名稱",
"delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?"
"delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?",
"see_more": "有關更多詳細資訊,請參閱 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
"swagger_ui": "ETAPI Swagger UI"
},
"options_widget": {
"options_status": "選項狀態",

View File

@@ -309,6 +309,7 @@
"delete_all_revisions": "Видалити всі версії цієї нотатки",
"delete_all_button": "Видалити всі версії",
"help_title": "Довідка щодо Версій нотаток",
"revision_last_edited": "Цю версію востаннє редагували {{date}}",
"confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
"no_revisions": "Поки що немає версій цієї нотатки...",
"restore_button": "Відновити",
@@ -1402,6 +1403,10 @@
"etapi": {
"title": "ETAPI",
"description": "ETAPI — це REST API, який використовується для програмного доступу до екземпляра Trilium без інтерфейсу користувача.",
"see_more": "Див. докладнішу інформацію у {{- link_to_wiki}} та {{- link_to_openapi_spec}} або {{- link_to_swagger_ui }}.",
"wiki": "вікі",
"openapi_spec": "ETAPI OpenAPI spec",
"swagger_ui": "ETAPI Swagger UI",
"create_token": "Створити новий токен ETAPI",
"existing_tokens": "Існуючі токени",
"no_tokens_yet": "Токенів поки що немає. Натисніть кнопку вище, щоб створити його.",

View File

@@ -26,6 +26,7 @@ interface CustomGlobals {
appContext: AppContext;
froca: Froca;
treeCache: Froca;
importMarkdownInline: () => Promise<unknown>;
SEARCH_HELP_TEXT: string;
activeDialog: JQuery<HTMLElement> | null;
componentId: string;

View File

@@ -79,7 +79,6 @@ export default function ExportDialog() {
values={[
{ value: "html", label: t("export.format_html_zip") },
{ value: "markdown", label: t("export.format_markdown") },
{ value: "share", label: t("export.share-format") },
{ value: "opml", label: t("export.format_opml") }
]}
/>

View File

@@ -7,7 +7,6 @@ import utils from "../../services/utils";
import Modal from "../react/Modal";
import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks";
import EditableTextTypeWidget from "../type_widgets/editable_text";
interface RenderMarkdownResponse {
htmlContent: string;
@@ -15,34 +14,39 @@ interface RenderMarkdownResponse {
export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
const [ text, setText ] = useState("");
const [ shown, setShown ] = useState(false);
useTriliumEvent("showPasteMarkdownDialog", ({ textTypeWidget }) => {
setTextTypeWidget(textTypeWidget);
const triggerImport = useCallback(() => {
if (appContext.tabManager.getActiveContextNoteType() !== "text") {
return;
}
if (utils.isElectron()) {
const { clipboard } = utils.dynamicRequire("electron");
const text = clipboard.readText();
convertMarkdownToHtml(text, textTypeWidget);
convertMarkdownToHtml(text);
} else {
setShown(true);
}
});
}, []);
useTriliumEvent("importMarkdownInline", triggerImport);
useTriliumEvent("pasteMarkdownIntoText", triggerImport);
async function sendForm() {
await convertMarkdownToHtml(text);
setText("");
setShown(false);
}
return (
<Modal
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={sendForm} keyboardShortcut="Ctrl+Space" />}
onShown={() => markdownImportTextArea.current?.focus()}
onHidden={async () => {
if (textTypeWidget) {
await convertMarkdownToHtml(text, textTypeWidget);
}
setShown(false);
setText("");
}}
onHidden={() => setShown(false) }
show={shown}
>
<p>{t("markdown_import.modal_body_text")}</p>
@@ -52,17 +56,26 @@ export default function MarkdownImportDialog() {
onKeyDown={(e) => {
if (e.key === "Enter" && e.ctrlKey) {
e.preventDefault();
setShown(false);
sendForm();
}
}}></textarea>
</Modal>
)
}
async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: EditableTextTypeWidget) {
async function convertMarkdownToHtml(markdownContent: string) {
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
await textTypeWidget.addHtmlToEditor(htmlContent);
const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
if (!textEditor) {
return;
}
const viewFragment = textEditor.data.processor.toView(htmlContent);
const modelFragment = textEditor.data.toModel(viewFragment);
textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
textEditor.editing.view.focus();
toast.showMessage(t("markdown_import.import_success"));
}

View File

@@ -155,11 +155,6 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
return Promise.resolve();
}
// Avoid not showing recent notes when creating a new empty tab.
if ("noteContext" in data && data.noteContext.ntxId !== "_popup-editor") {
return Promise.resolve();
}
return super.handleEventInChildren(name, data);
}

View File

@@ -140,10 +140,11 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
<FormList onSelect={onSelect} fullHeight>
{revisions.map((item) =>
<FormListItem
title={t("revisions.revision_last_edited", { date: item.dateLastEdited })}
value={item.revisionId}
active={currentRevision && item.revisionId === currentRevision.revisionId}
>
{item.dateCreated && item.dateCreated.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
{item.dateLastEdited && item.dateLastEdited.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
</FormListItem>
)}
</FormList>);

View File

@@ -147,12 +147,6 @@ const categories: Category[] = [
];
const icons: Icon[] = [
{
name: "empty",
slug: "empty",
category_id: 113,
type_of_icon: "REGULAR"
},
{
name: "child",
slug: "child-regular",

View File

@@ -56,16 +56,4 @@
.note-icon-widget .icon-list span:hover {
border: 1px solid var(--main-border-color);
}
.note-icon-widget .icon-list span.bx-empty {
width: unset;
}
.note-icon-widget .icon-list span.bx-empty::before {
display: inline-block;
content: "";
border: 1px dashed var(--muted-text-color);
width: 1em;
height: 1em;
}

View File

@@ -264,6 +264,7 @@
position: absolute;
inset-inline-end: 5px;
bottom: 5px;
z-index: 1000;
}
.style-resolver {

View File

@@ -329,30 +329,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
});
}
async addHtmlToEditor(html: string) {
await this.initialized;
const editor = this.watchdog.editor;
if (!editor) return;
editor.model.change((writer) => {
const viewFragment = editor.data.processor.toView(html);
const modelFragment = editor.data.toModel(viewFragment);
const insertPosition = editor.model.document.selection.getLastPosition();
if (insertPosition) {
const range = editor.model.insertContent(modelFragment, insertPosition);
if (range) {
writer.setSelection(range.end);
}
}
});
editor.editing.view.focus();
}
addTextToActiveEditorEvent({ text }: EventData<"addTextToActiveEditor">) {
if (!this.isActive()) {
return;
@@ -409,10 +385,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.triggerCommand("showAddLinkDialog", { textTypeWidget: this, text: selectedText });
}
pasteMarkdownIntoTextCommand() {
this.triggerCommand("showPasteMarkdownDialog", { textTypeWidget: this });
}
getSelectedText() {
const range = this.watchdog.editor?.model.document.selection.getFirstRange();
let text = "";

View File

@@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact";
import { CSSProperties } from "preact/compat";
interface OptionsSectionProps {
title?: ComponentChildren;
title?: string;
children: ComponentChildren;
noCard?: boolean;
style?: CSSProperties;

View File

@@ -11,7 +11,6 @@ import dialog from "../../../services/dialog";
import { formatDateTime } from "../../../utils/formatters";
import ActionButton from "../../react/ActionButton";
import { useTriliumEvent } from "../../react/hooks";
import HelpButton from "../../react/HelpButton";
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
@@ -49,13 +48,19 @@ export default function EtapiSettings() {
message: t("etapi.token_created_message"),
defaultValue: authToken
});
}, []);
}, []);
return (
<OptionsSection title={t("etapi.title")}>
<FormText>
{t("etapi.description")}
<HelpButton helpPage="pgxEVkzLl1OP" />
{t("etapi.description")}<br />
<RawHtml
html={t("etapi.see_more", {
link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
// TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
})} />
</FormText>
<Button
@@ -63,7 +68,6 @@ export default function EtapiSettings() {
text={t("etapi.create_token")}
onClick={createTokenCallback}
/>
<hr />
<h5>{t("etapi.existing_tokens")}</h5>
@@ -119,7 +123,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
text={t("etapi.rename_token")}
onClick={() => renameCallback(etapiTokenId, name)}
/>
<ActionButton
icon="bx bx-trash"
text={t("etapi.delete_token")}

View File

@@ -74,6 +74,7 @@ export default defineConfig(() => ({
mobile: join(__dirname, "src", "mobile.ts"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
share: join(__dirname, "src", "share.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts"),
print: join(__dirname, "src", "print.tsx")
@@ -83,8 +84,7 @@ export default defineConfig(() => ({
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]",
manualChunks: {
"ckeditor5": [ "@triliumnext/ckeditor5" ],
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
"ckeditor5": [ "@triliumnext/ckeditor5" ]
},
},
onwarn(warning, rollupWarn) {

View File

@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
:POWERSHELL
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
GOTO END
:BATCH

View File

@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
:POWERSHELL
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
GOTO END
:BATCH

View File

@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
:POWERSHELL
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
GOTO END
:BATCH

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "38.5.0",
"electron": "38.3.0",
"@electron-forge/cli": "7.10.2",
"@electron-forge/maker-deb": "7.10.2",
"@electron-forge/maker-dmg": "7.10.2",

View File

@@ -11,7 +11,6 @@ async function main() {
// Copy assets.
build.copy("src/assets", "assets/");
build.copy("/apps/server/src/assets", "assets/");
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
// Copy node modules dependencies

View File

@@ -13,7 +13,7 @@
"devDependencies": {
"@types/better-sqlite3": "7.6.13",
"@types/mime-types": "3.0.1",
"@types/yargs": "17.0.34"
"@types/yargs": "17.0.33"
},
"scripts": {
"dev": "tsx src/main.ts",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "38.5.0",
"electron": "38.3.0",
"fs-extra": "11.3.2"
},
"scripts": {

View File

@@ -6,7 +6,7 @@ import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js
import debounce from "@triliumnext/client/src/services/debounce.js";
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
import cls from "@triliumnext/server/src/services/cls.js";
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip.js";
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
@@ -23,8 +23,6 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
}
const BASE_URL = "https://docs.triliumnotes.org";
const NOTE_MAPPINGS: NoteMapping[] = [
{
rootNoteId: "pOsGYCXsbNQG",
@@ -77,7 +75,7 @@ async function setOptions() {
optionsService.setOption("compressImages", "false");
}
async function exportData(noteId: string, format: ExportFormat, outputPath: string, ignoredFiles?: Set<string>) {
async function exportData(noteId: string, format: "html" | "markdown", outputPath: string, ignoredFiles?: Set<string>) {
const zipFilePath = "output.zip";
try {
@@ -160,14 +158,6 @@ async function cleanUpMeta(outputPath: string, minify: boolean) {
}
el.isExpanded = false;
// Rewrite web view URLs that point to root.
if (el.type === "webView" && minify) {
const srcAttr = el.attributes.find(attr => attr.name === "webViewSrc");
if (srcAttr.value.startsWith("/")) {
srcAttr.value = BASE_URL + srcAttr.value;
}
}
}
if (minify) {

View File

@@ -1,4 +1,4 @@
FROM node:24.11.0-bullseye-slim AS builder
FROM node:22.20.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.11.0-bullseye-slim
FROM node:22.20.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@@ -1,4 +1,4 @@
FROM node:24.11.0-alpine AS builder
FROM node:22.20.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.11.0-alpine
FROM node:22.20.0-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@@ -1,4 +1,4 @@
FROM node:24.11.0-alpine AS builder
FROM node:22.20.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.11.0-alpine
FROM node:22.20.0-alpine
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -1,28 +0,0 @@
FROM node:22.21.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
WORKDIR /usr/src/app/build
COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.21.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gosu && \
rm -rf \
/var/lib/apt/lists/* \
/var/cache/apt/*
WORKDIR /usr/src/app
COPY ./dist /usr/src/app
RUN rm -rf /usr/src/app/node_modules/better-sqlite3
COPY --from=builder /usr/src/app/node_modules/better-sqlite3 /usr/src/app/node_modules/better-sqlite3
COPY ./start-docker.sh /usr/src/app
# Configure container
EXPOSE 8080
CMD [ "sh", "./start-docker.sh" ]
HEALTHCHECK --start-period=10s CMD exec gosu node node /usr/src/app/docker_healthcheck.cjs

View File

@@ -1,4 +1,4 @@
FROM node:24.11.0-bullseye-slim AS builder
FROM node:22.20.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.11.0-bullseye-slim
FROM node:22.20.0-bullseye-slim
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@@ -26,23 +26,21 @@
},
"dependencies": {
"better-sqlite3": "12.4.1",
"html-to-text": "9.0.5",
"node-html-parser": "7.0.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.68.0",
"@anthropic-ai/sdk": "0.67.0",
"@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.3",
"@preact/preset-vite": "2.10.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/turndown-plugin-gfm": "workspace:*",
"@types/archiver": "7.0.0",
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.13",
"@types/cls-hooked": "4.3.9",
"@types/compression": "1.8.1",
"@types/cookie-parser": "1.4.10",
"@types/cookie-parser": "1.4.9",
"@types/debounce": "1.2.4",
"@types/ejs": "3.1.5",
"@types/escape-html": "1.0.4",
@@ -58,37 +56,39 @@
"@types/sanitize-html": "2.16.0",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
"@types/serve-static": "1.15.9",
"@types/session-file-store": "1.2.5",
"@types/stream-throttle": "0.1.4",
"@types/supertest": "6.0.3",
"@types/swagger-ui-express": "4.1.8",
"@types/tmp": "0.2.6",
"@types/turndown": "5.0.6",
"@types/turndown": "5.0.5",
"@types/ws": "8.18.1",
"@types/xml2js": "0.4.14",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"axios": "1.13.1",
"axios": "1.12.2",
"bindings": "1.5.0",
"bootstrap": "5.3.8",
"chardet": "2.1.1",
"chardet": "2.1.0",
"cheerio": "1.1.2",
"chokidar": "4.0.3",
"cls-hooked": "4.2.2",
"compression": "1.8.1",
"cookie-parser": "1.4.7",
"csrf-csrf": "3.2.2",
"dayjs": "1.11.19",
"dayjs": "1.11.18",
"debounce": "2.2.0",
"debug": "4.4.3",
"ejs": "3.1.10",
"electron": "38.5.0",
"electron": "38.3.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.1.0",
"express-http-proxy": "2.1.2",
"express-openid-connect": "2.19.2",
"express-rate-limit": "8.2.1",
"express-rate-limit": "8.1.0",
"express-session": "1.18.2",
"file-uri-to-path": "2.0.0",
"fs-extra": "11.3.2",
@@ -100,7 +100,7 @@
"i18next": "25.6.0",
"i18next-fs-backend": "2.6.0",
"image-type": "6.0.0",
"ini": "6.0.0",
"ini": "5.0.0",
"is-animated": "2.0.2",
"is-svg": "6.1.0",
"jimp": "1.6.0",
@@ -109,8 +109,8 @@
"mime-types": "3.0.1",
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.2",
"openai": "6.7.0",
"ollama": "0.6.0",
"openai": "6.6.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
@@ -122,11 +122,12 @@
"striptags": "3.2.0",
"supertest": "7.1.4",
"swagger-jsdoc": "6.2.8",
"swagger-ui-express": "5.0.1",
"time2fa": "1.4.2",
"tmp": "0.2.5",
"turndown": "7.2.2",
"turndown": "7.2.1",
"unescape": "1.0.1",
"vite": "7.1.12",
"vite": "7.1.11",
"ws": "8.18.3",
"xml2js": "0.6.2",
"yauzl": "3.2.0"

View File

@@ -7,7 +7,6 @@ async function main() {
// Copy assets
build.copy("src/assets", "assets/");
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
// Copy node modules dependencies

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: Internal Trilium API
version: 0.99.3
title: Trilium Notes Internal API
version: 0.98.0
description: |
This is the internal API used by the Trilium Notes client application.
@@ -24,12 +24,11 @@ info:
State-changing operations require CSRF tokens when using session authentication.
contact:
name: Trilium Notes Team
email: contact@eliandoran.me
url: https://triliumnotes.org
name: TriliumNext Issue Tracker
url: https://github.com/TriliumNext/Trilium/issues
license:
name: GNU Affero General Public License v3.0 only
url: https://www.gnu.org/licenses/agpl-3.0.en.html
name: GNU Affero General Public License v3.0
url: https://www.gnu.org/licenses/agpl-3.0.html
servers:
- url: http://localhost:8080

View File

@@ -146,9 +146,228 @@ CREATE INDEX IDX_notes_blobId on notes (blobId);
CREATE INDEX IDX_revisions_blobId on revisions (blobId);
CREATE INDEX IDX_attachments_blobId on attachments (blobId);
-- Strategic Performance Indexes from migration 234
-- NOTES TABLE INDEXES
CREATE INDEX IDX_notes_search_composite
ON notes (isDeleted, type, mime, dateModified DESC);
CREATE INDEX IDX_notes_metadata_covering
ON notes (noteId, isDeleted, type, mime, title, dateModified, isProtected);
CREATE INDEX IDX_notes_protected_deleted
ON notes (isProtected, isDeleted)
WHERE isProtected = 1;
-- BRANCHES TABLE INDEXES
CREATE INDEX IDX_branches_tree_traversal
ON branches (parentNoteId, isDeleted, notePosition);
CREATE INDEX IDX_branches_covering
ON branches (noteId, parentNoteId, isDeleted, notePosition, prefix);
CREATE INDEX IDX_branches_note_parents
ON branches (noteId, isDeleted)
WHERE isDeleted = 0;
-- ATTRIBUTES TABLE INDEXES
CREATE INDEX IDX_attributes_search_composite
ON attributes (name, value, isDeleted);
CREATE INDEX IDX_attributes_covering
ON attributes (noteId, name, value, type, isDeleted, position);
CREATE INDEX IDX_attributes_inheritable
ON attributes (isInheritable, isDeleted)
WHERE isInheritable = 1 AND isDeleted = 0;
CREATE INDEX IDX_attributes_labels
ON attributes (type, name, value)
WHERE type = 'label' AND isDeleted = 0;
CREATE INDEX IDX_attributes_relations
ON attributes (type, name, value)
WHERE type = 'relation' AND isDeleted = 0;
-- BLOBS TABLE INDEXES
CREATE INDEX IDX_blobs_content_size
ON blobs (blobId, LENGTH(content));
-- ATTACHMENTS TABLE INDEXES
CREATE INDEX IDX_attachments_composite
ON attachments (ownerId, role, isDeleted, position);
-- REVISIONS TABLE INDEXES
CREATE INDEX IDX_revisions_note_date
ON revisions (noteId, utcDateCreated DESC);
-- ENTITY_CHANGES TABLE INDEXES
CREATE INDEX IDX_entity_changes_sync
ON entity_changes (isSynced, utcDateChanged);
CREATE INDEX IDX_entity_changes_component
ON entity_changes (componentId, utcDateChanged DESC);
-- RECENT_NOTES TABLE INDEXES
CREATE INDEX IDX_recent_notes_date
ON recent_notes (utcDateCreated DESC);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data TEXT,
expires INTEGER
);
-- FTS5 Full-Text Search Support
-- Create FTS5 virtual table with trigram tokenizer
-- Trigram tokenizer provides language-agnostic substring matching:
-- 1. Fast substring matching (50-100x speedup for LIKE queries without wildcards)
-- 2. Case-insensitive search without custom collation
-- 3. No language-specific stemming assumptions (works for all languages)
-- 4. Boolean operators (AND, OR, NOT) and phrase matching with quotes
--
-- IMPORTANT: Trigram requires minimum 3-character tokens for matching
-- detail='none' reduces index size by ~50% while maintaining MATCH/rank performance
-- (loses position info for highlight() function, but snippet() still works)
CREATE VIRTUAL TABLE notes_fts USING fts5(
noteId UNINDEXED,
title,
content,
tokenize = 'trigram',
detail = 'none'
);
-- Triggers to keep FTS table synchronized with notes
-- IMPORTANT: These triggers must handle all SQL operations including:
-- - Regular INSERT/UPDATE/DELETE
-- - INSERT OR REPLACE
-- - INSERT ... ON CONFLICT ... DO UPDATE (upsert)
-- - Cases where notes are created before blobs (import scenarios)
-- Trigger for INSERT operations on notes
-- Handles: INSERT, INSERT OR REPLACE, INSERT OR IGNORE, and the INSERT part of upsert
CREATE TRIGGER notes_fts_insert
AFTER INSERT ON notes
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND NEW.isDeleted = 0
AND NEW.isProtected = 0
BEGIN
-- First delete any existing FTS entry (in case of INSERT OR REPLACE)
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
-- Then insert the new entry, using LEFT JOIN to handle missing blobs
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
END;
-- Trigger for UPDATE operations on notes table
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
-- Fires for ANY update to searchable notes to ensure FTS stays in sync
CREATE TRIGGER notes_fts_update
AFTER UPDATE ON notes
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
-- Fire on any change, not just specific columns, to handle all upsert scenarios
BEGIN
-- Always delete the old entry
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
-- Insert new entry if note is not deleted and not protected
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '') -- Use empty string if blob doesn't exist yet
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId
WHERE NEW.isDeleted = 0
AND NEW.isProtected = 0;
END;
-- Trigger for UPDATE operations on blobs
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
-- IMPORTANT: Uses INSERT OR REPLACE for efficiency with deduplicated blobs
CREATE TRIGGER notes_fts_blob_update
AFTER UPDATE ON blobs
BEGIN
-- Use INSERT OR REPLACE for atomic update of all notes sharing this blob
-- This is more efficient than DELETE + INSERT when many notes share the same blob
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
SELECT
n.noteId,
n.title,
NEW.content
FROM notes n
WHERE n.blobId = NEW.blobId
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND n.isDeleted = 0
AND n.isProtected = 0;
END;
-- Trigger for DELETE operations
CREATE TRIGGER notes_fts_delete
AFTER DELETE ON notes
BEGIN
DELETE FROM notes_fts WHERE noteId = OLD.noteId;
END;
-- Trigger for soft delete (isDeleted = 1)
CREATE TRIGGER notes_fts_soft_delete
AFTER UPDATE ON notes
WHEN OLD.isDeleted = 0 AND NEW.isDeleted = 1
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
END;
-- Trigger for notes becoming protected
-- Remove from FTS when a note becomes protected
CREATE TRIGGER notes_fts_protect
AFTER UPDATE ON notes
WHEN OLD.isProtected = 0 AND NEW.isProtected = 1
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
END;
-- Trigger for notes becoming unprotected
-- Add to FTS when a note becomes unprotected (if eligible)
CREATE TRIGGER notes_fts_unprotect
AFTER UPDATE ON notes
WHEN OLD.isProtected = 1 AND NEW.isProtected = 0
AND NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND NEW.isDeleted = 0
BEGIN
DELETE FROM notes_fts WHERE noteId = NEW.noteId;
INSERT INTO notes_fts (noteId, title, content)
SELECT
NEW.noteId,
NEW.title,
COALESCE(b.content, '')
FROM (SELECT NEW.noteId) AS note_select
LEFT JOIN blobs b ON b.blobId = NEW.blobId;
END;
-- Trigger for INSERT operations on blobs
-- Handles: INSERT, INSERT OR REPLACE, and the INSERT part of upsert
-- Updates all notes that reference this blob (common during import and deduplication)
CREATE TRIGGER notes_fts_blob_insert
AFTER INSERT ON blobs
BEGIN
-- Use INSERT OR REPLACE to handle both new and existing FTS entries
-- This is crucial for blob deduplication where multiple notes may already
-- exist that reference this blob before the blob itself is created
INSERT OR REPLACE INTO notes_fts (noteId, title, content)
SELECT
n.noteId,
n.title,
NEW.content
FROM notes n
WHERE n.blobId = NEW.blobId
AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
AND n.isDeleted = 0
AND n.isProtected = 0;
END;

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 172 KiB

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 237 KiB

View File

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 202 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

View File

@@ -11,12 +11,12 @@
<p>To set your preferred chat model, you'll want to enter the provider's
name here:</p>
<figure class="image image_resized" style="width:88.38%;">
<img style="aspect-ratio:1884/1267;" src="Providers_image.png"
<img style="aspect-ratio:1884/1267;" src="AI Provider Information_im.png"
width="1884" height="1267">
</figure>
<p>And to set your preferred embedding provider:</p>
<figure class="image image_resized"
style="width:93.47%;">
<img style="aspect-ratio:1907/1002;" src="1_Providers_image.png"
<img style="aspect-ratio:1907/1002;" src="1_AI Provider Information_im.png"
width="1907" height="1002">
</figure>

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