Merge remote-tracking branch 'origin/main' into react/type_widgets

This commit is contained in:
Elian Doran 2025-11-06 11:33:57 +02:00
commit 5e83e6fa34
No known key found for this signature in database
743 changed files with 18960 additions and 10561 deletions

View File

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

View File

@ -1,6 +1,4 @@
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages name: Deploy Documentation
# This workflow builds and deploys your MkDocs site when changes are pushed to main
name: Deploy MkDocs Documentation
on: on:
# Trigger on push to main branch # Trigger on push to main branch
@ -11,11 +9,9 @@ on:
# Only run when docs files change # Only run when docs files change
paths: paths:
- 'docs/**' - 'docs/**'
- 'README.md' # README is synced to docs/index.md - 'apps/edit-docs/**'
- 'mkdocs.yml' - 'apps/build-docs/**'
- 'requirements-docs.txt' - 'packages/share-theme/**'
- '.github/workflows/deploy-docs.yml'
- 'scripts/fix-mkdocs-structure.ts'
# Allow manual triggering from Actions tab # Allow manual triggering from Actions tab
workflow_dispatch: workflow_dispatch:
@ -27,15 +23,13 @@ on:
- master - master
paths: paths:
- 'docs/**' - 'docs/**'
- 'README.md' # README is synced to docs/index.md - 'apps/edit-docs/**'
- 'mkdocs.yml' - 'apps/build-docs/**'
- 'requirements-docs.txt' - 'packages/share-theme/**'
- '.github/workflows/deploy-docs.yml'
- 'scripts/fix-mkdocs-structure.ts'
jobs: jobs:
build-and-deploy: build-and-deploy:
name: Build and Deploy MkDocs name: Build and Deploy Documentation
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
@ -49,72 +43,27 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v5 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 - name: Setup pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
# Setup Node.js with pnpm
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: '22' node-version: '24'
cache: 'pnpm' cache: 'pnpm'
# Install Node.js dependencies for the TypeScript script
- name: Install Dependencies - name: Install Dependencies
run: | run: pnpm install --frozen-lockfile
pnpm install --frozen-lockfile
- name: Fix Documentation Structure - name: Trigger build of documentation
run: | run: pnpm docs:build
# 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 - name: Validate Built Site
run: | run: |
# Basic validation that important files exist
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1) 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 -f site/developer-guide/index.html || (echo "ERROR: site/developer-guide/index.html not found" && exit 1)
test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1) echo "✓ User Guide and Developer Guide built successfully"
echo "✅ Site validation passed"
- name: Deploy - name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages uses: ./.github/actions/deploy-to-cloudflare-pages

View File

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

View File

@ -46,7 +46,7 @@ jobs:
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 24
cache: "pnpm" cache: "pnpm"
- name: Install npm dependencies - name: Install npm dependencies
@ -116,10 +116,10 @@ jobs:
- dockerfile: Dockerfile - dockerfile: Dockerfile
platform: linux/arm64 platform: linux/arm64
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
- dockerfile: Dockerfile - dockerfile: Dockerfile.legacy
platform: linux/arm/v7 platform: linux/arm/v7
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
- dockerfile: Dockerfile - dockerfile: Dockerfile.legacy
platform: linux/arm/v8 platform: linux/arm/v8
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
runs-on: ${{ matrix.image }} runs-on: ${{ matrix.image }}
@ -146,7 +146,7 @@ jobs:
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 24
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies

View File

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

View File

@ -24,7 +24,7 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6 - uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 24
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies

View File

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

View File

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

2
.nvmrc
View File

@ -1 +1 @@
22.21.0 24.11.0

View File

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

View File

@ -1,15 +0,0 @@
{
"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

@ -0,0 +1,22 @@
{
"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.20.0",
"devDependencies": {
"@redocly/cli": "2.11.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

@ -0,0 +1,36 @@
/**
* 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

@ -0,0 +1,147 @@
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 importAndExportDocs(sourcePath: string, outputSubDir: string) {
const note = await importData(sourcePath);
// Use a meaningful name for the temporary zip file
const zipName = outputSubDir || "user-guide";
const zipFilePath = `output-${zipName}.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);
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
const outputPath = outputSubDir ? join(OUTPUT_DIR, outputSubDir) : OUTPUT_DIR;
await extractZip(zipFilePath, outputPath);
} finally {
if (await fsExtra.exists(zipFilePath)) {
await fsExtra.rm(zipFilePath);
}
}
}
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);
// Wait for becca to be loaded before importing data
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
// Build User Guide
console.log("Building User Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
// Build Developer Guide
console.log("Building Developer Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
// Copy favicon.
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
console.log("Documentation built successfully!");
}
export async function importData(path: string) {
const buffer = await createImportZip(path);
const importService = (await import("../../server/src/services/import/zip.js")).default;
const TaskContext = (await import("../../server/src/services/task_context.js")).default;
const context = new TaskContext("no-progress-reporting", "importNotes", null);
const becca = (await import("../../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

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

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 "../../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

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="refresh" content="0; url=/user-guide">
<title>Redirecting...</title>
</head>
<body>
<p>If you are not redirected automatically, <a href="/user-guide">click here</a>.</p>
</body>
</html>

View File

@ -0,0 +1,30 @@
import { join } from "path";
import BuildContext from "./context";
import buildSwagger from "./swagger";
import { cpSync, 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);
// Copy index and 404 files.
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
}
main();

View File

@ -0,0 +1,15 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,36 @@
{
"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

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import { SqlExecuteResults } from "@triliumnext/commons";
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx"; import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx"; import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
import { ReactWrappedWidget } from "../widgets/basic_widget.js"; import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import { TypeWidget } from "../widgets/note_types.jsx"; import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootContainer; getRootWidget: (appContext: AppContext) => RootContainer;
@ -219,12 +219,12 @@ export type CommandMappings = {
/** Works only in the electron context menu. */ /** Works only in the electron context menu. */
replaceMisspelling: CommandData; replaceMisspelling: CommandData;
importMarkdownInline: CommandData;
showPasswordNotSet: CommandData; showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData; showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string }; showUploadAttachmentsDialog: CommandData & { noteId: string };
showIncludeNoteDialog: CommandData & IncludeNoteOpts; showIncludeNoteDialog: CommandData & IncludeNoteOpts;
showAddLinkDialog: CommandData & AddLinkOpts; showAddLinkDialog: CommandData & AddLinkOpts;
showPasteMarkdownDialog: CommandData & MarkdownImportOpts;
closeProtectedSessionPasswordDialog: CommandData; closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData; copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData; copyImageToClipboard: CommandData;
@ -271,6 +271,7 @@ export type CommandMappings = {
closeThisNoteSplit: CommandData; closeThisNoteSplit: CommandData;
moveThisNoteSplit: CommandData & { isMovingLeft: boolean }; moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
jumpToNote: CommandData; jumpToNote: CommandData;
openTodayNote: CommandData;
commandPalette: CommandData; commandPalette: CommandData;
// Keyboard shortcuts // Keyboard shortcuts

View File

@ -159,6 +159,16 @@ export default class Entrypoints extends Component {
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" }); 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() { async runActiveNoteCommand() {
const noteContext = appContext.tabManager.getActiveContext(); const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) { if (!noteContext) {

View File

@ -417,7 +417,7 @@ export default class FNote {
return notePaths; return notePaths;
} }
getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] { getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root"; const isHoistedRoot = hoistedNoteId === "root";
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
@ -428,7 +428,23 @@ export default class FNote {
isHidden: path.includes("_hidden") 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) => { 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) { if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1; return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isArchived !== b.isArchived) { } else if (a.isArchived !== b.isArchived) {
@ -449,10 +465,11 @@ export default class FNote {
* Returns the note path considered to be the "best" * Returns the note path considered to be the "best"
* *
* @param {string} [hoistedNoteId='root'] * @param {string} [hoistedNoteId='root']
* @param {string|null} [activeNotePath=null]
* @return {string[]} array of noteIds constituting the particular note path * @return {string[]} array of noteIds constituting the particular note path
*/ */
getBestNotePath(hoistedNoteId = "root") { getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath; return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
} }
/** /**

View File

@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "editBranchPrefix", command: "editBranchPrefix",
keyboardShortcut: "editBranchPrefix", keyboardShortcut: "editBranchPrefix",
uiIcon: "bx bx-rename", uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
}, },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp }, { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },

View File

@ -1,3 +1,5 @@
@import "boxicons/css/boxicons.min.css";
:root { :root {
--print-font-size: 11pt; --print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important; --ck-content-color-image-caption-background: transparent !important;

View File

@ -56,7 +56,20 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
await import("@triliumnext/ckeditor5/src/theme/ck-content.css"); await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
} }
const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true }); const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
containerRef.current?.replaceChildren(...$renderedContent); 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 });
});
})
);
} }
load().then(() => requestAnimationFrame(onReady)) load().then(() => requestAnimationFrame(onReady))

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 "../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,9 +20,6 @@ function setupGlobs() {
window.glob.froca = froca; window.glob.froca = froca;
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while 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) { window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = String(msg).toLowerCase(); const string = String(msg).toLowerCase();

View File

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

View File

@ -159,7 +159,7 @@ describe("shortcuts", () => {
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy(); expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
// Special keys // Special keys
for (const keyCode of [ "Delete", "Enter" ]) { for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
event = createKeyboardEvent({ key: keyCode, code: keyCode }); event = createKeyboardEvent({ key: keyCode, code: keyCode });
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy(); expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
} }

View File

@ -46,6 +46,7 @@ for (let i = 1; i <= 19; i++) {
const KEYCODES_WITH_NO_MODIFIER = new Set([ const KEYCODES_WITH_NO_MODIFIER = new Set([
"Delete", "Delete",
"Enter", "Enter",
"NumpadEnter",
...functionKeyCodes ...functionKeyCodes
]); ]);

View File

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

View File

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

View File

@ -1,84 +0,0 @@
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

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

View File

@ -716,7 +716,6 @@
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان" "backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
}, },
"etapi": { "etapi": {
"wiki": "ويكي",
"created": "تم الأنشاء", "created": "تم الأنشاء",
"actions": "أجراءات", "actions": "أجراءات",
"title": "ETAPI", "title": "ETAPI",

View File

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

View File

@ -4,7 +4,7 @@
"homepage": "Startseite:", "homepage": "Startseite:",
"app_version": "App-Version:", "app_version": "App-Version:",
"db_version": "DB-Version:", "db_version": "DB-Version:",
"sync_version": "Synch-version:", "sync_version": "Sync-Version:",
"build_date": "Build-Datum:", "build_date": "Build-Datum:",
"build_revision": "Build-Revision:", "build_revision": "Build-Revision:",
"data_directory": "Datenverzeichnis:" "data_directory": "Datenverzeichnis:"
@ -104,7 +104,8 @@
"export_status": "Exportstatus", "export_status": "Exportstatus",
"export_in_progress": "Export läuft: {{progressCount}}", "export_in_progress": "Export läuft: {{progressCount}}",
"export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.", "export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
"format_pdf": "PDF - für Ausdrucke oder Teilen." "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."
}, },
"help": { "help": {
"noteNavigation": "Notiz Navigation", "noteNavigation": "Notiz Navigation",
@ -260,7 +261,6 @@
"delete_all_revisions": "Lösche alle Revisionen dieser Notiz", "delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
"delete_all_button": "Alle Revisionen löschen", "delete_all_button": "Alle Revisionen löschen",
"help_title": "Hilfe zu Notizrevisionen", "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?", "confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
"no_revisions": "Für diese Notiz gibt es noch keine Revisionen...", "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.", "confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
@ -991,7 +991,7 @@
"enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:", "enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
"start_session_button": "Starte eine geschützte Sitzung", "start_session_button": "Starte eine geschützte Sitzung",
"started": "Geschützte Sitzung gestartet.", "started": "Geschützte Sitzung gestartet.",
"wrong_password": "Passwort flasch.", "wrong_password": "Passwort falsch.",
"protecting-finished-successfully": "Geschützt erfolgreich beendet.", "protecting-finished-successfully": "Geschützt erfolgreich beendet.",
"unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.", "unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.",
"protecting-in-progress": "Schützen läuft: {{count}}", "protecting-in-progress": "Schützen läuft: {{count}}",
@ -1286,10 +1286,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.", "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", "create_token": "Erstelle ein neues ETAPI-Token",
"existing_tokens": "Vorhandene Token", "existing_tokens": "Vorhandene Token",
"no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.", "no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
@ -1658,7 +1654,7 @@
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen", "add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
"cut": "Ausschneiden", "cut": "Ausschneiden",
"copy": "Kopieren", "copy": "Kopieren",
"copy-link": "Link opieren", "copy-link": "Link kopieren",
"paste": "Einfügen", "paste": "Einfügen",
"paste-as-plain-text": "Als unformatierten Text einfügen", "paste-as-plain-text": "Als unformatierten Text einfügen",
"search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten" "search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"

View File

@ -36,10 +36,13 @@
}, },
"branch_prefix": { "branch_prefix": {
"edit_branch_prefix": "Edit branch prefix", "edit_branch_prefix": "Edit branch prefix",
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
"help_on_tree_prefix": "Help on Tree prefix", "help_on_tree_prefix": "Help on Tree prefix",
"prefix": "Prefix: ", "prefix": "Prefix: ",
"save": "Save", "save": "Save",
"branch_prefix_saved": "Branch prefix has been saved." "branch_prefix_saved": "Branch prefix has been saved.",
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
"affected_branches": "Affected branches ({{count}}):"
}, },
"bulk_actions": { "bulk_actions": {
"bulk_actions": "Bulk actions", "bulk_actions": "Bulk actions",
@ -104,7 +107,8 @@
"export_status": "Export status", "export_status": "Export status",
"export_in_progress": "Export in progress: {{progressCount}}", "export_in_progress": "Export in progress: {{progressCount}}",
"export_finished_successfully": "Export finished successfully.", "export_finished_successfully": "Export finished successfully.",
"format_pdf": "PDF - for printing or sharing purposes." "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."
}, },
"help": { "help": {
"title": "Cheatsheet", "title": "Cheatsheet",
@ -260,7 +264,6 @@
"delete_all_revisions": "Delete all revisions of this note", "delete_all_revisions": "Delete all revisions of this note",
"delete_all_button": "Delete all revisions", "delete_all_button": "Delete all revisions",
"help_title": "Help on Note 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?", "confirm_delete_all": "Do you want to delete all revisions of this note?",
"no_revisions": "No revisions for this note yet...", "no_revisions": "No revisions for this note yet...",
"restore_button": "Restore", "restore_button": "Restore",
@ -1453,10 +1456,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.", "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", "create_token": "Create new ETAPI token",
"existing_tokens": "Existing tokens", "existing_tokens": "Existing tokens",
"no_tokens_yet": "There are no tokens yet. Click on the button above to create one.", "no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",
@ -2038,6 +2037,9 @@
"start-presentation": "Start presentation", "start-presentation": "Start presentation",
"slide-overview": "Toggle an overview of the slides" "slide-overview": "Toggle an overview of the slides"
}, },
"calendar_view": {
"delete_note": "Delete note..."
},
"command_palette": { "command_palette": {
"tree-action-name": "Tree: {{name}}", "tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note", "export_note_title": "Export Note",

View File

@ -104,7 +104,8 @@
"export_status": "Estado de exportación", "export_status": "Estado de exportación",
"export_in_progress": "Exportación en curso: {{progressCount}}", "export_in_progress": "Exportación en curso: {{progressCount}}",
"export_finished_successfully": "La exportación finalizó exitosamente.", "export_finished_successfully": "La exportación finalizó exitosamente.",
"format_pdf": "PDF - para propósitos de impresión o compartición." "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."
}, },
"help": { "help": {
"noteNavigation": "Navegación de notas", "noteNavigation": "Navegación de notas",
@ -184,7 +185,8 @@
}, },
"import-status": "Estado de importación", "import-status": "Estado de importación",
"in-progress": "Importación en progreso: {{progress}}", "in-progress": "Importación en progreso: {{progress}}",
"successful": "Importación finalizada exitosamente." "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."
}, },
"include_note": { "include_note": {
"dialog_title": "Incluir nota", "dialog_title": "Incluir nota",
@ -259,7 +261,6 @@
"delete_all_revisions": "Eliminar todas las revisiones de esta nota", "delete_all_revisions": "Eliminar todas las revisiones de esta nota",
"delete_all_button": "Eliminar todas las revisiones", "delete_all_button": "Eliminar todas las revisiones",
"help_title": "Ayuda sobre revisiones de notas", "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?", "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
"no_revisions": "Aún no hay revisiones para esta nota...", "no_revisions": "Aún no hay revisiones para esta nota...",
"restore_button": "Restaurar", "restore_button": "Restaurar",
@ -1445,10 +1446,6 @@
"etapi": { "etapi": {
"title": "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.", "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", "create_token": "Crear nuevo token ETAPI",
"existing_tokens": "Tokens existentes", "existing_tokens": "Tokens existentes",
"no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.", "no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
@ -1715,7 +1712,9 @@
"window-on-top": "Mantener esta ventana en la parte superior" "window-on-top": "Mantener esta ventana en la parte superior"
}, },
"note_detail": { "note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'" "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.."
}, },
"note_title": { "note_title": {
"placeholder": "escriba el título de la nota aquí..." "placeholder": "escriba el título de la nota aquí..."

View File

@ -260,7 +260,6 @@
"delete_all_revisions": "Supprimer toutes les versions de cette note", "delete_all_revisions": "Supprimer toutes les versions de cette note",
"delete_all_button": "Supprimer toutes les versions", "delete_all_button": "Supprimer toutes les versions",
"help_title": "Aide sur les versions de notes", "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 ?", "confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
"no_revisions": "Aucune version pour cette note pour l'instant...", "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.", "confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
@ -1289,8 +1288,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.", "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", "create_token": "Créer un nouveau jeton ETAPI",
"existing_tokens": "Jetons existants", "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.", "no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
@ -1307,9 +1304,7 @@
"delete_token": "Supprimer/désactiver ce token", "delete_token": "Supprimer/désactiver ce token",
"rename_token_title": "Renommer le jeton", "rename_token_title": "Renommer le jeton",
"rename_token_message": "Veuillez saisir le nom du nouveau 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_widget": {
"options_status": "Statut des options", "options_status": "Statut des options",

View File

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

View File

@ -109,7 +109,8 @@
"export_type_single": "Solo questa nota, senza le sottostanti", "export_type_single": "Solo questa nota, senza le sottostanti",
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.", "format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
"opml_version_1": "OPML v.1.0 - solo testo semplice", "opml_version_1": "OPML v.1.0 - solo testo semplice",
"opml_version_2": "OPML v2.0 - supporta anche HTML" "opml_version_2": "OPML v2.0 - supporta anche HTML",
"share-format": "HTML per la pubblicazione sul web - utilizza lo stesso tema utilizzato per le note condivise, ma può essere pubblicato come sito web statico."
}, },
"password_not_set": { "password_not_set": {
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.", "body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
@ -132,10 +133,6 @@
"new_token_message": "Inserisci il nome del nuovo token", "new_token_message": "Inserisci il nome del nuovo token",
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.", "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", "create_token": "Crea un nuovo token ETAPI",
"existing_tokens": "Token esistenti", "existing_tokens": "Token esistenti",
"no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.", "no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
@ -867,7 +864,6 @@
"delete_all_revisions": "Elimina tutte le revisioni di questa nota", "delete_all_revisions": "Elimina tutte le revisioni di questa nota",
"delete_all_button": "Elimina tutte le revisioni", "delete_all_button": "Elimina tutte le revisioni",
"help_title": "Aiuto sulle revisioni delle note", "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?", "confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
"no_revisions": "Ancora nessuna revisione per questa nota...", "no_revisions": "Ancora nessuna revisione per questa nota...",
"restore_button": "Ripristina", "restore_button": "Ripristina",

View File

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

View File

@ -13,6 +13,13 @@
"critical-error": { "critical-error": {
"title": "Kritische 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." "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": { "add_link": {

View File

@ -912,7 +912,6 @@
"delete_all_revisions": "Usuń wszystkie wersje tej notatki", "delete_all_revisions": "Usuń wszystkie wersje tej notatki",
"delete_all_button": "Usuń wszystkie wersje", "delete_all_button": "Usuń wszystkie wersje",
"help_title": "Pomoc dotycząca wersji notatki", "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?", "confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
"no_revisions": "Brak wersji dla tej notatki...", "no_revisions": "Brak wersji dla tej notatki...",
"restore_button": "Przywróć", "restore_button": "Przywróć",
@ -1664,10 +1663,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.", "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", "create_token": "Utwórz nowy token ETAPI",
"existing_tokens": "Istniejące tokeny", "existing_tokens": "Istniejące tokeny",
"no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.", "no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",

View File

@ -259,7 +259,6 @@
"delete_all_revisions": "Apagar todas as versões desta nota", "delete_all_revisions": "Apagar todas as versões desta nota",
"delete_all_button": "Apagar todas as versões", "delete_all_button": "Apagar todas as versões",
"help_title": "Ajuda sobre as versões da nota", "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?", "confirm_delete_all": "Quer apagar todas as versões desta nota?",
"no_revisions": "Ainda não há versões para esta nota...", "no_revisions": "Ainda não há versões para esta nota...",
"restore_button": "Recuperar", "restore_button": "Recuperar",
@ -1423,10 +1422,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.", "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", "create_token": "Criar token ETAPI",
"existing_tokens": "Tokens existentes", "existing_tokens": "Tokens existentes",
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.", "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",

View File

@ -415,7 +415,6 @@
"delete_all_revisions": "Excluir todas as versões desta nota", "delete_all_revisions": "Excluir todas as versões desta nota",
"delete_all_button": "Excluir todas as versões", "delete_all_button": "Excluir todas as versões",
"help_title": "Ajuda sobre as versões da nota", "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?", "confirm_delete_all": "Você quer excluir todas as versões desta nota?",
"no_revisions": "Ainda não há versões para esta nota...", "no_revisions": "Ainda não há versões para esta nota...",
"restore_button": "Recuperar", "restore_button": "Recuperar",
@ -1933,10 +1932,6 @@
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.", "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", "create_token": "Criar novo token ETAPI",
"existing_tokens": "Tokens existentes", "existing_tokens": "Tokens existentes",
"no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.", "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",

View File

@ -507,17 +507,13 @@
"new_token_message": "Introduceți denumirea noului token", "new_token_message": "Introduceți denumirea noului token",
"new_token_title": "Token ETAPI nou", "new_token_title": "Token ETAPI nou",
"no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.", "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": "Redenumește token-ul",
"rename_token_message": "Introduceți denumirea noului token", "rename_token_message": "Introduceți denumirea noului token",
"rename_token_title": "Redenumire 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", "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_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_created_title": "Token ETAPI creat",
"token_name": "Denumire token", "token_name": "Denumire token"
"wiki": "wiki"
}, },
"execute_script": { "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:", "example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
@ -1090,7 +1086,6 @@
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.", "preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
"restore_button": "Restaurează", "restore_button": "Restaurează",
"revision_deleted": "Revizia notiței a fost ștearsă.", "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ă.", "revision_restored": "Revizia notiței a fost restaurată.",
"revisions_deleted": "Notița reviziei a fost ștearsă.", "revisions_deleted": "Notița reviziei a fost ștearsă.",
"maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.", "maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",

View File

@ -366,7 +366,6 @@
"delete_all_button": "Удалить все версии", "delete_all_button": "Удалить все версии",
"help_title": "Помощь по версиям заметок", "help_title": "Помощь по версиям заметок",
"confirm_delete_all": "Вы хотите удалить все версии этой заметки?", "confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
"revision_last_edited": "Эта версия последний раз редактировалась {{date}}",
"confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.", "confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
"confirm_delete": "Вы хотите удалить эту версию?", "confirm_delete": "Вы хотите удалить эту версию?",
"revisions_deleted": "Версии заметки были удалены.", "revisions_deleted": "Версии заметки были удалены.",
@ -1441,7 +1440,6 @@
}, },
"etapi": { "etapi": {
"title": "ETAPI", "title": "ETAPI",
"wiki": "вики",
"created": "Создано", "created": "Создано",
"actions": "Действия", "actions": "Действия",
"existing_tokens": "Существующие токены", "existing_tokens": "Существующие токены",
@ -1449,10 +1447,7 @@
"default_token_name": "новый токен", "default_token_name": "новый токен",
"rename_token_title": "Переименовать токен", "rename_token_title": "Переименовать токен",
"description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.", "description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
"see_more": "Более подробную информацию смотрите в {{- link_to_wiki}} и {{- link_to_openapi_spec}} или {{- link_to_swagger_ui }}.",
"create_token": "Создать новый токен ETAPI", "create_token": "Создать новый токен ETAPI",
"openapi_spec": "Спецификация ETAPI OpenAPI",
"swagger_ui": "Пользовательский интерфейс ETAPI Swagger",
"new_token_title": "Новый токен ETAPI", "new_token_title": "Новый токен ETAPI",
"token_created_title": "Создан токен ETAPI", "token_created_title": "Создан токен ETAPI",
"rename_token": "Переименовать этот токен", "rename_token": "Переименовать этот токен",

View File

@ -256,7 +256,6 @@
"delete_all_revisions": "Obriši sve revizije ove beleške", "delete_all_revisions": "Obriši sve revizije ove beleške",
"delete_all_button": "Obriši sve revizije", "delete_all_button": "Obriši sve revizije",
"help_title": "Pomoć za Revizije beleški", "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?", "confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
"no_revisions": "Još uvek nema revizija za ovu belešku...", "no_revisions": "Još uvek nema revizija za ovu belešku...",
"restore_button": "Vrati", "restore_button": "Vrati",

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
import FNote from "../../../entities/fnote";
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
import link_context_menu from "../../../menus/link_context_menu";
import branches from "../../../services/branches";
import { t } from "../../../services/i18n";
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
e.preventDefault();
e.stopPropagation();
contextMenu.show({
x: e.pageX,
y: e.pageY,
items: [
...link_context_menu.getItems(),
{ kind: "separator" },
{
title: t("calendar_view.delete_note"),
uiIcon: "bx bx-trash",
handler: async () => {
const branchId = parentNote.childToBranch[noteId];
await branches.deleteNotes([ branchId ], false, false);
}
}
],
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
})
}

View File

@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton"; import ActionButton from "../../react/ActionButton";
import { RefObject } from "preact"; import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar"; import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
interface CalendarViewData { interface CalendarViewData {
@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const plugins = usePlugins(isEditable, isCalendarRoot); const plugins = usePlugins(isEditable, isCalendarRoot);
const locale = useLocale(); const locale = useLocale();
const { eventDidMount } = useEventDisplayCustomization(); const { eventDidMount } = useEventDisplayCustomization(note);
const editingProps = useEditing(note, isEditable, isCalendarRoot); const editingProps = useEditing(note, isEditable, isCalendarRoot);
// React to changes. // React to changes.
@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
}; };
} }
function useEventDisplayCustomization() { function useEventDisplayCustomization(parentNote: FNote) {
const eventDidMount = useCallback((e: EventMountArg) => { const eventDidMount = useCallback((e: EventMountArg) => {
const { iconClass, promotedAttributes } = e.event.extendedProps; const { iconClass, promotedAttributes } = e.event.extendedProps;
@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
} }
$(mainContainer ?? e.el).append($(promotedAttributesHtml)); $(mainContainer ?? e.el).append($(promotedAttributesHtml));
} }
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
const noteId = e.event.extendedProps.noteId;
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
});
}, []); }, []);
return { eventDidMount }; return { eventDidMount };
} }

View File

@ -0,0 +1,13 @@
.branch-prefix-dialog .branch-prefix-notes-list {
margin-top: 10px;
}
.branch-prefix-dialog .branch-prefix-notes-list ul {
max-height: 200px;
overflow: auto;
margin-top: 5px;
}
.branch-prefix-dialog .branch-prefix-current {
opacity: 0.6;
}

View File

@ -10,53 +10,86 @@ import Button from "../react/Button.jsx";
import FormGroup from "../react/FormGroup.js"; import FormGroup from "../react/FormGroup.js";
import { useTriliumEvent } from "../react/hooks.jsx"; import { useTriliumEvent } from "../react/hooks.jsx";
import FBranch from "../../entities/fbranch.js"; import FBranch from "../../entities/fbranch.js";
import type { ContextMenuCommandData } from "../../components/app_context.js";
import "./branch_prefix.css";
// Virtual branches (e.g., from search results) start with this prefix
const VIRTUAL_BRANCH_PREFIX = "virt-";
export default function BranchPrefixDialog() { export default function BranchPrefixDialog() {
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
const [ branch, setBranch ] = useState<FBranch>(); const [ branches, setBranches ] = useState<FBranch[]>([]);
const [ prefix, setPrefix ] = useState(""); const [ prefix, setPrefix ] = useState("");
const branchInput = useRef<HTMLInputElement>(null); const branchInput = useRef<HTMLInputElement>(null);
useTriliumEvent("editBranchPrefix", async () => { useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
const notePath = appContext.tabManager.getActiveContextNotePath(); let branchIds: string[] = [];
if (!notePath) {
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
// Multi-select mode from tree context menu
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
} else {
// Single branch mode from keyboard shortcut or when no selection
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
return;
}
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
if (!noteId || !parentNoteId) {
return;
}
const branchId = await froca.getBranchId(parentNoteId, noteId);
if (!branchId) {
return;
}
const parentNote = await froca.getNote(parentNoteId);
if (!parentNote || parentNote.type === "search") {
return;
}
branchIds = [branchId];
}
if (branchIds.length === 0) {
return; return;
} }
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath); const newBranches = branchIds
.map(id => froca.getBranch(id))
.filter((branch): branch is FBranch => branch !== null);
if (!noteId || !parentNoteId) { if (newBranches.length === 0) {
return; return;
} }
const newBranchId = await froca.getBranchId(parentNoteId, noteId); setBranches(newBranches);
if (!newBranchId) { // Use the prefix of the first branch as the initial value
return; setPrefix(newBranches[0]?.prefix ?? "");
}
const parentNote = await froca.getNote(parentNoteId);
if (!parentNote || parentNote.type === "search") {
return;
}
const newBranch = froca.getBranch(newBranchId);
setBranch(newBranch);
setPrefix(newBranch?.prefix ?? "");
setShown(true); setShown(true);
}); });
async function onSubmit() { async function onSubmit() {
if (!branch) { if (branches.length === 0) {
return; return;
} }
savePrefix(branch.branchId, prefix); if (branches.length === 1) {
await savePrefix(branches[0].branchId, prefix);
} else {
await savePrefixBatch(branches.map(b => b.branchId), prefix);
}
setShown(false); setShown(false);
} }
const isSingleBranch = branches.length === 1;
return ( return (
<Modal <Modal
className="branch-prefix-dialog" className="branch-prefix-dialog"
title={t("branch_prefix.edit_branch_prefix")} title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
size="lg" size="lg"
onShown={() => branchInput.current?.focus()} onShown={() => branchInput.current?.focus()}
onHidden={() => setShown(false)} onHidden={() => setShown(false)}
@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
<div class="input-group"> <div class="input-group">
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput} <input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} /> onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div> {isSingleBranch && branches[0] && (
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
)}
</div> </div>
</FormGroup> </FormGroup>
{!isSingleBranch && (
<div className="branch-prefix-notes-list">
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
<ul>
{branches.map((branch) => {
const note = branch.getNoteFromCache();
return (
<li key={branch.branchId}>
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
{note.title}
</li>
);
})}
</ul>
</div>
)}
</Modal> </Modal>
); );
} }
@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix }); await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved")); toast.showMessage(t("branch_prefix.branch_prefix_saved"));
} }
async function savePrefixBatch(branchIds: string[], prefix: string) {
await server.put("branches/set-prefix-batch", { branchIds, prefix });
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
}

View File

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

View File

@ -1,5 +1,4 @@
import { useCallback, useRef, useState } from "preact/hooks"; import { useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import server from "../../services/server"; import server from "../../services/server";
import toast from "../../services/toast"; import toast from "../../services/toast";
@ -7,6 +6,12 @@ import utils from "../../services/utils";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import Button from "../react/Button"; import Button from "../react/Button";
import { useTriliumEvent } from "../react/hooks"; import { useTriliumEvent } from "../react/hooks";
import EditableTextTypeWidget from "../type_widgets/editable_text";
import { CKEditorApi } from "../type_widgets/text/CKEditorWithWatchdog";
export interface MarkdownImportOpts {
editorApi: CKEditorApi;
}
interface RenderMarkdownResponse { interface RenderMarkdownResponse {
htmlContent: string; htmlContent: string;
@ -14,39 +19,36 @@ interface RenderMarkdownResponse {
export default function MarkdownImportDialog() { export default function MarkdownImportDialog() {
const markdownImportTextArea = useRef<HTMLTextAreaElement>(null); const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
const editorApiRef = useRef<CKEditorApi>(null);
const [ textTypeWidget, setTextTypeWidget ] = useState<EditableTextTypeWidget>();
const [ text, setText ] = useState(""); const [ text, setText ] = useState("");
const [ shown, setShown ] = useState(false); const [ shown, setShown ] = useState(false);
const triggerImport = useCallback(() => { useTriliumEvent("showPasteMarkdownDialog", ({ editorApi }) => {
if (appContext.tabManager.getActiveContextNoteType() !== "text") { setTextTypeWidget(textTypeWidget);
return;
}
if (utils.isElectron()) { if (utils.isElectron()) {
const { clipboard } = utils.dynamicRequire("electron"); const { clipboard } = utils.dynamicRequire("electron");
const text = clipboard.readText(); const text = clipboard.readText();
convertMarkdownToHtml(text); convertMarkdownToHtml(text, editorApi);
} else { } else {
editorApiRef.current = editorApi;
setShown(true); setShown(true);
} }
}, []); });
useTriliumEvent("importMarkdownInline", triggerImport);
useTriliumEvent("pasteMarkdownIntoText", triggerImport);
async function sendForm() {
await convertMarkdownToHtml(text);
setText("");
setShown(false);
}
return ( return (
<Modal <Modal
className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg" 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={sendForm} keyboardShortcut="Ctrl+Space" />} footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
onShown={() => markdownImportTextArea.current?.focus()} onShown={() => markdownImportTextArea.current?.focus()}
onHidden={() => setShown(false) } onHidden={async () => {
if (editorApiRef.current) {
await convertMarkdownToHtml(text, editorApiRef.current);
}
setShown(false);
setText("");
}}
show={shown} show={shown}
> >
<p>{t("markdown_import.modal_body_text")}</p> <p>{t("markdown_import.modal_body_text")}</p>
@ -56,26 +58,15 @@ export default function MarkdownImportDialog() {
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && e.ctrlKey) { if (e.key === "Enter" && e.ctrlKey) {
e.preventDefault(); e.preventDefault();
sendForm(); setShown(false);
} }
}}></textarea> }}></textarea>
</Modal> </Modal>
) )
} }
async function convertMarkdownToHtml(markdownContent: string) { async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: CKEditorApi) {
const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent }); const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
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")); toast.showMessage(t("markdown_import.import_success"));
} }

View File

@ -154,6 +154,11 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
return Promise.resolve(); 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); return super.handleEventInChildren(name, data);
} }

View File

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

View File

@ -1591,6 +1591,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.clearSelectedNodes(); this.clearSelectedNodes();
} }
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
if (!branchIds.length) {
return;
}
// Trigger the event with the selected branch IDs
appContext.triggerEvent("editBranchPrefix", {
selectedOrActiveBranchIds: branchIds,
node: node
});
}
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) { canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
if (node.data.noteId === "root") { if (node.data.noteId === "root") {
return false; return false;

View File

@ -13,8 +13,8 @@ export default function EditedNotesTab({ note }: TabContext) {
useEffect(() => { useEffect(() => {
if (!note) return; if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => { server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId); editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId); const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes); setEditedNotes(editedNotes);
}); });
@ -41,11 +41,11 @@ export default function EditedNotesTab({ note }: TabContext) {
)} )}
</span> </span>
) )
}))} }), " ")}
</div> </div>
) : ( ) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div> <div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)} )}
</div> </div>
) )
} }

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import dialog from "../../../services/dialog";
import { formatDateTime } from "../../../utils/formatters"; import { formatDateTime } from "../../../utils/formatters";
import ActionButton from "../../react/ActionButton"; import ActionButton from "../../react/ActionButton";
import { useTriliumEvent } from "../../react/hooks"; import { useTriliumEvent } from "../../react/hooks";
import HelpButton from "../../react/HelpButton";
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>; type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>; type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
@ -48,19 +49,13 @@ export default function EtapiSettings() {
message: t("etapi.token_created_message"), message: t("etapi.token_created_message"),
defaultValue: authToken defaultValue: authToken
}); });
}, []); }, []);
return ( return (
<OptionsSection title={t("etapi.title")}> <OptionsSection title={t("etapi.title")}>
<FormText> <FormText>
{t("etapi.description")}<br /> {t("etapi.description")}
<RawHtml <HelpButton helpPage="pgxEVkzLl1OP" />
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> </FormText>
<Button <Button
@ -68,6 +63,7 @@ export default function EtapiSettings() {
text={t("etapi.create_token")} text={t("etapi.create_token")}
onClick={createTokenCallback} onClick={createTokenCallback}
/> />
<hr /> <hr />
<h5>{t("etapi.existing_tokens")}</h5> <h5>{t("etapi.existing_tokens")}</h5>
@ -123,7 +119,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
text={t("etapi.rename_token")} text={t("etapi.rename_token")}
onClick={() => renameCallback(etapiTokenId, name)} onClick={() => renameCallback(etapiTokenId, name)}
/> />
<ActionButton <ActionButton
icon="bx bx-trash" icon="bx bx-trash"
text={t("etapi.delete_token")} text={t("etapi.delete_token")}

View File

@ -72,8 +72,8 @@ function EditorFeatures() {
return ( return (
<OptionsSection title={t("editorfeatures.title")}> <OptionsSection title={t("editorfeatures.title")}>
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} /> <EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} /> <EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.note_completion_description")} />
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.emoji_completion_description")} /> <EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.slash_commands_description")} />
</OptionsSection> </OptionsSection>
); );
} }

View File

@ -13,6 +13,7 @@ export interface CKEditorApi {
getSelectedText(): string; getSelectedText(): string;
addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): void; addLink(notePath: string, linkTitle: string | null, externalLink?: boolean): void;
addLinkToEditor(linkHref: string, linkTitle: string): void; addLinkToEditor(linkHref: string, linkTitle: string): void;
addHtmlToEditor(html: string): void;
addIncludeNote(noteId: string, boxSize?: BoxSize): void; addIncludeNote(noteId: string, boxSize?: BoxSize): void;
addImage(noteId: string): Promise<void>; addImage(noteId: string): Promise<void>;
} }
@ -100,6 +101,26 @@ export default function CKEditorWithWatchdog({ containerRef: externalContainerRe
); );
}); });
}, },
addHtmlToEditor(html: string) {
const editor = watchdogRef.current?.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();
},
async addImage(noteId) { async addImage(noteId) {
const editor = watchdogRef.current?.editor; const editor = watchdogRef.current?.editor;
if (!editor) return; if (!editor) return;

View File

@ -90,6 +90,12 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
} }
}); });
}, },
pasteMarkdownIntoTextCommand() {
if (!editorApiRef.current) return;
parentComponent?.triggerCommand("showPasteMarkdownDialog", {
editorApi: editorApiRef.current,
});
},
// Include note functionality note // Include note functionality note
addIncludeNoteToTextCommand() { addIncludeNoteToTextCommand() {
if (!editorApiRef.current) return; if (!editorApiRef.current) return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{ {
"formatVersion": 2, "formatVersion": 2,
"appVersion": "0.99.2", "appVersion": "0.99.3",
"files": [ "files": [
{ {
"isClone": false, "isClone": false,
@ -2700,6 +2700,7 @@
} }
], ],
"format": "html", "format": "html",
"dataFileName": "Note Types.html",
"attachments": [], "attachments": [],
"dirFileName": "Note Types", "dirFileName": "Note Types",
"children": [ "children": [

View File

@ -270,7 +270,7 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>Note Types <li><a href="root/Trilium%20Demo/Note%20Types.html" target="detail">Note Types</a>
<ul> <ul>
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a> <li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
</li> </li>

View File

@ -14,6 +14,7 @@
<div class="ck-content"> <div class="ck-content">
<h2>☑️ Tasks</h2> <h2>☑️ Tasks</h2>
<ul> <ul>
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li> <li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
</ul> </ul>

View File

@ -14,11 +14,10 @@
<div class="ck-content"> <div class="ck-content">
<figure class="image image-style-align-right image_resized" style="width:29.84%;"> <figure class="image image-style-align-right image_resized" style="width:29.84%;">
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg" width="150" <img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg"
height="150"> width="150" height="150">
</figure> </figure>
<p><strong>Welcome to Trilium Notes!</strong> <p><strong>Welcome to Trilium Notes!</strong>
</p> </p>
<p>This is a "demo" document packaged with Trilium to showcase some of its <p>This is a "demo" document packaged with Trilium to showcase some of its
features and also give you some ideas on how you might structure your notes. features and also give you some ideas on how you might structure your notes.
@ -26,22 +25,17 @@
you wish.</p> you wish.</p>
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or <p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
our <a href="https://github.com/TriliumNext">GitHub repository</a> our <a href="https://github.com/TriliumNext">GitHub repository</a>
</p> </p>
<h2>Cleanup</h2> <h2>Cleanup</h2>
<p>Once you're finished with experimenting and want to cleanup these pages, <p>Once you're finished with experimenting and want to cleanup these pages,
you can simply delete them all.</p> you can simply delete them all.</p>
<h2>Formatting</h2> <h2>Formatting</h2>
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>. <p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or&nbsp; You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or&nbsp;
<a <a
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p> class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
<h3>Lists</h3> <h3>Lists</h3>
<p><strong>Ordered:</strong> <p><strong>Ordered:</strong>
</p> </p>
<ol> <ol>
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li> <li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
@ -56,7 +50,6 @@
</li> </li>
</ol> </ol>
<p><strong>Unordered:</strong> <p><strong>Unordered:</strong>
</p> </p>
<ul> <ul>
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li> <li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
@ -67,7 +60,6 @@
</li> </li>
</ul> </ul>
<h3>Block quotes</h3> <h3>Block quotes</h3>
<blockquote> <blockquote>
<p>Whereof one cannot speak, thereof one must be silent”</p> <p>Whereof one cannot speak, thereof one must be silent”</p>
<p> Ludwig Wittgenstein</p> <p> Ludwig Wittgenstein</p>
@ -75,9 +67,9 @@
<hr> <hr>
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>, <p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
<a <a
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and
<a <a
href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and <a href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p> href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
</div> </div>
</div> </div>
</body> </body>

View File

@ -21,8 +21,12 @@
language, should that fail it is possible to manually adjust it. The color language, should that fail it is possible to manually adjust it. The color
scheme for the syntax highlighting is adjustable in settings.&nbsp;</p><pre><code class="language-application-javascript-env-frontend">function helloWorld() { scheme for the syntax highlighting is adjustable in settings.&nbsp;</p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
alert("Hello world"); alert("Hello world");
}</code></pre> }</code></pre>
<p>For larger pieces of code it is better to use a code note, which uses <p>For larger pieces of code it is better to use a code note, which uses
a fully-fledged code editor (CodeMirror). For an example of a code note, a fully-fledged code editor (CodeMirror). For an example of a code note,

View File

@ -0,0 +1,21 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../style.css">
<base target="_parent">
<title data-trilium-title>Note Types</title>
</head>
<body>
<div class="content">
<h1 data-trilium-h1>Note Types</h1>
<div class="ck-content">
<p>T</p>
</div>
</div>
</body>
</html>

View File

@ -13,9 +13,8 @@
<h1 data-trilium-h1>Task manager</h1> <h1 data-trilium-h1>Task manager</h1>
<div class="ck-content"> <div class="ck-content">
<p>This is a simple TODO/Task manager. You can see some description and explanation <p>This is a simple TODO/Task manager. See the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases/task-manager">Trilium documentation</a> for
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a> information on how it works.</p>
</p>
<p>Please note that this is meant as scripting example only and feature/bug <p>Please note that this is meant as scripting example only and feature/bug
support is very limited.</p> support is very limited.</p>
</div> </div>

View File

@ -16,18 +16,32 @@
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a> <p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
</p><pre><code class="language-text-x-sh">#!/bin/bash </p><pre><code class="language-text-x-sh">#!/bin/bash
# This script opens 4 terminal windows. # This script opens 4 terminal windows.
i="0" i="0"
while [ $i -lt 4 ] while [ $i -lt 4 ]
do do
xterm &amp; xterm &amp;
i=$[$i+1] i=$[$i+1]
done</code></pre> done</code></pre>
</div> </div>
</div> </div>

View File

@ -12,11 +12,11 @@
"@triliumnext/desktop": "workspace:*", "@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1", "copy-webpack-plugin": "13.0.1",
"electron": "38.4.0", "electron": "38.5.0",
"fs-extra": "11.3.2" "fs-extra": "11.3.2"
}, },
"scripts": { "scripts": {
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts", "edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
"edit-demo": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts" "edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
} }
} }

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

View File

@ -0,0 +1,502 @@
import { test, expect } from "@playwright/test";
import App from "./support/app";
const BASE_URL = "http://127.0.0.1:8082";
/**
* E2E tests for exact search functionality using the leading "=" operator.
*
* These tests validate the GitHub issue:
* - Searching for "pagio" returns many false positives (e.g., "page", "pages")
* - Searching for "=pagio" should return ONLY exact matches for "pagio"
*/
test.describe("Exact Search with Leading = Operator", () => {
let csrfToken: string;
let createdNoteIds: string[] = [];
test.beforeEach(async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
// Get CSRF token
csrfToken = await page.evaluate(() => {
return (window as any).glob.csrfToken;
});
expect(csrfToken).toBeTruthy();
// Create test notes with specific content patterns
// Note 1: Contains exactly "pagio" in title
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Test Note with pagio",
content: "This note contains the word pagio in the content.",
type: "text"
}
});
expect(note1.ok()).toBeTruthy();
const note1Data = await note1.json();
createdNoteIds.push(note1Data.note.noteId);
// Note 2: Contains "page" (not exact match)
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Test Note with page",
content: "This note contains the word page in the content.",
type: "text"
}
});
expect(note2.ok()).toBeTruthy();
const note2Data = await note2.json();
createdNoteIds.push(note2Data.note.noteId);
// Note 3: Contains "pages" (plural, not exact match)
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Test Note with pages",
content: "This note contains the word pages in the content.",
type: "text"
}
});
expect(note3.ok()).toBeTruthy();
const note3Data = await note3.json();
createdNoteIds.push(note3Data.note.noteId);
// Note 4: Contains "homepage" (contains "page", not exact match)
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Homepage Note",
content: "This note is about homepage content.",
type: "text"
}
});
expect(note4.ok()).toBeTruthy();
const note4Data = await note4.json();
createdNoteIds.push(note4Data.note.noteId);
// Note 5: Another note with exact "pagio" in content
const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Another pagio Note",
content: "This is another note with pagio content for testing exact matches.",
type: "text"
}
});
expect(note5.ok()).toBeTruthy();
const note5Data = await note5.json();
createdNoteIds.push(note5Data.note.noteId);
// Note 6: Contains "pagio" in title only
const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "pagio",
content: "This note has pagio as the title.",
type: "text"
}
});
expect(note6.ok()).toBeTruthy();
const note6Data = await note6.json();
createdNoteIds.push(note6Data.note.noteId);
// Wait a bit for indexing
await page.waitForTimeout(500);
});
test.afterEach(async ({ page }) => {
// Clean up created notes
for (const noteId of createdNoteIds) {
try {
const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
headers: { "x-csrf-token": csrfToken }
});
} catch (e) {
console.error(`Failed to delete note ${noteId}:`, e);
}
}
createdNoteIds = [];
});
test("Quick search without = operator returns all partial matches", async ({ page }) => {
// Test the /quick-search endpoint without the = operator
const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
// Should return multiple notes including "page", "pages", "homepage"
expect(data.searchResultNoteIds).toBeDefined();
expect(data.searchResults).toBeDefined();
// Filter to only our test notes
const testResults = data.searchResults.filter((result: any) =>
result.noteTitle.includes("page") ||
result.noteTitle.includes("pagio") ||
result.noteTitle.includes("Homepage")
);
// Should find at least "page", "pages", "homepage", and "pagio" notes
expect(testResults.length).toBeGreaterThanOrEqual(4);
console.log("Quick search 'pag' found:", testResults.length, "matching notes");
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
});
test("Quick search with = operator returns only exact matches", async ({ page }) => {
// Test the /quick-search endpoint WITH the = operator
const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
// Should return only notes with exact "pagio" match
expect(data.searchResultNoteIds).toBeDefined();
expect(data.searchResults).toBeDefined();
// Filter to only our test notes
const testResults = data.searchResults.filter((result: any) =>
createdNoteIds.includes(result.notePath.split("/").pop() || "")
);
console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
// Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
expect(testResults.length).toBe(3);
// Verify that none of the results contain "page" or "pages" (only "pagio")
for (const result of testResults) {
const title = result.noteTitle.toLowerCase();
const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
expect(hasPageNotPagio).toBe(false);
}
});
test("Full search API without = operator returns partial matches", async ({ page }) => {
// Test the /search endpoint without the = operator
const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
// Should return an array of note IDs
expect(Array.isArray(data)).toBe(true);
// Filter to only our test notes
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
// Should find at least 4 notes
expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
});
test("Full search API with = operator returns only exact matches", async ({ page }) => {
// Test the /search endpoint WITH the = operator
const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
// Should return an array of note IDs
expect(Array.isArray(data)).toBe(true);
// Filter to only our test notes
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
// Should find exactly 3 notes with exact "pagio" match
expect(testNoteIds.length).toBe(3);
});
test("Exact search operator works with content search", async ({ page }) => {
// Create a note with "test" in title but different content
const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Testing Content",
content: "This note contains the exact word test in content.",
type: "text"
}
});
expect(noteWithTest.ok()).toBeTruthy();
const noteWithTestData = await noteWithTest.json();
const testNoteId = noteWithTestData.note.noteId;
createdNoteIds.push(testNoteId);
// Create a note with "testing" (not exact match)
const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Testing More",
content: "This note has testing in the content.",
type: "text"
}
});
expect(noteWithTesting.ok()).toBeTruthy();
const noteWithTestingData = await noteWithTesting.json();
createdNoteIds.push(noteWithTestingData.note.noteId);
await page.waitForTimeout(500);
// Search with exact operator
const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const ourTestNotes = data.searchResults.filter((result: any) => {
const noteId = result.notePath.split("/").pop();
return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
});
console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
// Should find the note with exact "test" match, but not "testing"
// Note: This test may fail if the implementation doesn't properly handle exact matching in content
expect(ourTestNotes.length).toBeGreaterThan(0);
});
test("Exact search is case-insensitive", async ({ page }) => {
// Create notes with different case variations
const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "EXACT MATCH",
content: "This note has EXACT in uppercase.",
type: "text"
}
});
expect(noteUpper.ok()).toBeTruthy();
const noteUpperData = await noteUpper.json();
createdNoteIds.push(noteUpperData.note.noteId);
const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "exact match",
content: "This note has exact in lowercase.",
type: "text"
}
});
expect(noteLower.ok()).toBeTruthy();
const noteLowerData = await noteLower.json();
createdNoteIds.push(noteLowerData.note.noteId);
await page.waitForTimeout(500);
// Search with exact operator in lowercase
const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const ourTestNotes = data.searchResults.filter((result: any) => {
const noteId = result.notePath.split("/").pop();
return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
});
console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
// Should find both uppercase and lowercase versions
expect(ourTestNotes.length).toBe(2);
});
test("Exact phrase matching with multi-word searches", async ({ page }) => {
// Create notes with various phrase patterns
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "exact phrase",
content: "This note contains the exact phrase.",
type: "text"
}
});
expect(note1.ok()).toBeTruthy();
const note1Data = await note1.json();
createdNoteIds.push(note1Data.note.noteId);
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "exact phrase match",
content: "This note has exact phrase followed by more words.",
type: "text"
}
});
expect(note2.ok()).toBeTruthy();
const note2Data = await note2.json();
createdNoteIds.push(note2Data.note.noteId);
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "phrase exact",
content: "This note has the words in reverse order.",
type: "text"
}
});
expect(note3.ok()).toBeTruthy();
const note3Data = await note3.json();
createdNoteIds.push(note3Data.note.noteId);
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "this exact and that phrase",
content: "Words are separated but both present.",
type: "text"
}
});
expect(note4.ok()).toBeTruthy();
const note4Data = await note4.json();
createdNoteIds.push(note4Data.note.noteId);
await page.waitForTimeout(500);
// Search for exact phrase "exact phrase"
const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const ourTestNotes = data.searchResults.filter((result: any) => {
const noteId = result.notePath.split("/").pop();
return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
});
console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
// Should find only notes 1 and 2 (consecutive "exact phrase")
// Should NOT find note 3 (reversed order) or note 4 (words separated)
expect(ourTestNotes.length).toBe(2);
const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
expect(foundTitles).toContain("exact phrase");
expect(foundTitles).toContain("exact phrase match");
expect(foundTitles).not.toContain("phrase exact");
expect(foundTitles).not.toContain("this exact and that phrase");
});
test("Exact phrase matching respects word order", async ({ page }) => {
// Create notes to test word order sensitivity
const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Testing Order",
content: "This is a test sentence for verification.",
type: "text"
}
});
expect(noteForward.ok()).toBeTruthy();
const noteForwardData = await noteForward.json();
createdNoteIds.push(noteForwardData.note.noteId);
const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Order Testing",
content: "A sentence test is this for verification.",
type: "text"
}
});
expect(noteReverse.ok()).toBeTruthy();
const noteReverseData = await noteReverse.json();
createdNoteIds.push(noteReverseData.note.noteId);
await page.waitForTimeout(500);
// Search for exact phrase "test sentence"
const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const ourTestNotes = data.searchResults.filter((result: any) => {
const noteId = result.notePath.split("/").pop();
return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
});
console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
// Should find only the forward order note
expect(ourTestNotes.length).toBe(1);
expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
});
test("Multi-word exact search without quotes", async ({ page }) => {
// Test that multi-word search with = but without quotes also does exact phrase matching
const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Quick Test Note",
content: "A simple note for multi word testing.",
type: "text"
}
});
expect(notePhrase.ok()).toBeTruthy();
const notePhraseData = await notePhrase.json();
createdNoteIds.push(notePhraseData.note.noteId);
const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
headers: { "x-csrf-token": csrfToken },
data: {
title: "Word Multi Testing",
content: "Words are multi scattered in this testing example.",
type: "text"
}
});
expect(noteScattered.ok()).toBeTruthy();
const noteScatteredData = await noteScattered.json();
createdNoteIds.push(noteScatteredData.note.noteId);
await page.waitForTimeout(500);
// Search for "=multi word" without quotes (parser tokenizes as two words)
const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
headers: { "x-csrf-token": csrfToken }
});
expect(response.ok()).toBeTruthy();
const data = await response.json();
const ourTestNotes = data.searchResults.filter((result: any) => {
const noteId = result.notePath.split("/").pop();
return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
});
console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
// Should find only the note with consecutive "multi word" phrase
expect(ourTestNotes.length).toBe(1);
expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
});
});

View File

@ -1,4 +1,4 @@
FROM node:22.21.0-bullseye-slim AS builder FROM node:24.11.0-bullseye-slim AS builder
RUN corepack enable RUN corepack enable
# Install native dependencies since we might be building cross-platform. # 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 # We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.21.0-bullseye-slim FROM node:24.11.0-bullseye-slim
# Install only runtime dependencies # Install only runtime dependencies
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y --no-install-recommends \ apt-get install -y --no-install-recommends \

View File

@ -1,4 +1,4 @@
FROM node:22.21.0-alpine AS builder FROM node:24.11.0-alpine AS builder
RUN corepack enable RUN corepack enable
# Install native dependencies since we might be building cross-platform. # 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 # We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.21.0-alpine FROM node:24.11.0-alpine
# Install runtime dependencies # Install runtime dependencies
RUN apk add --no-cache su-exec shadow RUN apk add --no-cache su-exec shadow

View File

@ -1,4 +1,4 @@
FROM node:22.21.0-alpine AS builder FROM node:24.11.0-alpine AS builder
RUN corepack enable RUN corepack enable
# Install native dependencies since we might be building cross-platform. # 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 # We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.21.0-alpine FROM node:24.11.0-alpine
# Create a non-root user with configurable UID/GID # Create a non-root user with configurable UID/GID
ARG USER=trilium ARG USER=trilium
ARG UID=1001 ARG UID=1001

View File

@ -0,0 +1,28 @@
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:22.21.0-bullseye-slim AS builder FROM node:24.11.0-bullseye-slim AS builder
RUN corepack enable RUN corepack enable
# Install native dependencies since we might be building cross-platform. # 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 # We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.21.0-bullseye-slim FROM node:24.11.0-bullseye-slim
# Create a non-root user with configurable UID/GID # Create a non-root user with configurable UID/GID
ARG USER=trilium ARG USER=trilium
ARG UID=1001 ARG UID=1001

View File

@ -4,12 +4,12 @@ info:
title: ETAPI title: ETAPI
description: External Trilium API description: External Trilium API
contact: contact:
name: zadam name: Trilium Notes Team
email: zadam.apps@gmail.com email: contact@eliandoran.me
url: https://github.com/zadam/trilium url: https://triliumnotes.org
license: license:
name: Apache 2.0 name: GNU Affero General Public License v3.0 only
url: https://www.apache.org/licenses/LICENSE-2.0.html url: https://www.gnu.org/licenses/agpl-3.0.en.html
servers: servers:
- url: http://localhost:37740/etapi - url: http://localhost:37740/etapi
- url: http://localhost:8080/etapi - url: http://localhost:8080/etapi

View File

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

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