Compare commits
	
		
			16 Commits
		
	
	
		
			electron_n
			...
			feat/rice-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					10988095c2 | ||
| 
						 | 
					253da139de | ||
| 
						 | 
					d992a5e4a2 | ||
| 
						 | 
					58c225237c | ||
| 
						 | 
					d074841885 | ||
| 
						 | 
					06b2d71b27 | ||
| 
						 | 
					0afb8a11c8 | ||
| 
						 | 
					f529ddc601 | ||
| 
						 | 
					8572f82e0a | ||
| 
						 | 
					b09a2c386d | ||
| 
						 | 
					7c5553bd4b | ||
| 
						 | 
					37d0136c50 | ||
| 
						 | 
					5b79e0d71e | ||
| 
						 | 
					053f722cb8 | ||
| 
						 | 
					21aaec2c38 | ||
| 
						 | 
					1db4971da6 | 
							
								
								
									
										2
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -12,7 +12,7 @@ runs:
 | 
			
		||||
  - name: Set up node & dependencies
 | 
			
		||||
    uses: actions/setup-node@v6
 | 
			
		||||
    with:
 | 
			
		||||
      node-version: 24
 | 
			
		||||
      node-version: 22
 | 
			
		||||
      cache: "pnpm"
 | 
			
		||||
  - name: Install dependencies
 | 
			
		||||
    shell: bash
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								.github/workflows/deploy-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,4 +1,6 @@
 | 
			
		||||
name: Deploy Documentation
 | 
			
		||||
# GitHub Actions workflow for deploying MkDocs documentation to Cloudflare Pages
 | 
			
		||||
# This workflow builds and deploys your MkDocs site when changes are pushed to main
 | 
			
		||||
name: Deploy MkDocs Documentation
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  # Trigger on push to main branch
 | 
			
		||||
@@ -9,8 +11,11 @@ on:
 | 
			
		||||
    # Only run when docs files change
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'apps/edit-docs/**'
 | 
			
		||||
      - 'packages/share-theme/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
 | 
			
		||||
  # Allow manual triggering from Actions tab
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
@@ -22,12 +27,15 @@ on:
 | 
			
		||||
      - master
 | 
			
		||||
    paths:
 | 
			
		||||
      - 'docs/**'
 | 
			
		||||
      - 'apps/edit-docs/**'
 | 
			
		||||
      - 'packages/share-theme/**'
 | 
			
		||||
      - 'README.md'  # README is synced to docs/index.md
 | 
			
		||||
      - 'mkdocs.yml'
 | 
			
		||||
      - 'requirements-docs.txt'
 | 
			
		||||
      - '.github/workflows/deploy-docs.yml'
 | 
			
		||||
      - 'scripts/fix-mkdocs-structure.ts'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build-and-deploy:
 | 
			
		||||
    name: Build and Deploy Documentation
 | 
			
		||||
    name: Build and Deploy MkDocs
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    timeout-minutes: 10
 | 
			
		||||
 | 
			
		||||
@@ -41,25 +49,72 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout Repository
 | 
			
		||||
        uses: actions/checkout@v5
 | 
			
		||||
        with:
 | 
			
		||||
          fetch-depth: 0 # Fetch all history for git info and mkdocs-git-revision-date plugin
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v6
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.14'
 | 
			
		||||
          cache: 'pip'
 | 
			
		||||
          cache-dependency-path: 'requirements-docs.txt'
 | 
			
		||||
 | 
			
		||||
      - name: Install MkDocs and Dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          pip install --upgrade pip
 | 
			
		||||
          pip install -r requirements-docs.txt
 | 
			
		||||
        env:
 | 
			
		||||
          PIP_DISABLE_PIP_VERSION_CHECK: 1
 | 
			
		||||
 | 
			
		||||
      # Setup pnpm before fixing docs structure
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
 | 
			
		||||
      # Setup Node.js with pnpm
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '24'
 | 
			
		||||
          node-version: '22'
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
      # Install Node.js dependencies for the TypeScript script
 | 
			
		||||
      - name: Install Dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
        run: |
 | 
			
		||||
          pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
      - name: Trigger build of documentation
 | 
			
		||||
        run: pnpm docs:build
 | 
			
		||||
      - name: Fix Documentation Structure
 | 
			
		||||
        run: |
 | 
			
		||||
          # Fix duplicate navigation entries by moving overview pages to index.md
 | 
			
		||||
          pnpm run chore:fix-mkdocs-structure
 | 
			
		||||
 | 
			
		||||
      - name: Build MkDocs Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Build with strict mode but allow expected warnings
 | 
			
		||||
          mkdocs build --verbose || {
 | 
			
		||||
            EXIT_CODE=$?
 | 
			
		||||
            # Check if the only issue is expected warnings
 | 
			
		||||
            if mkdocs build 2>&1 | grep -E "WARNING.*(README|not found)" && \
 | 
			
		||||
               [ $(mkdocs build 2>&1 | grep -c "ERROR") -eq 0 ]; then
 | 
			
		||||
              echo "✅ Build succeeded with expected warnings"
 | 
			
		||||
              mkdocs build --verbose
 | 
			
		||||
            else
 | 
			
		||||
              echo "❌ Build failed with unexpected errors"
 | 
			
		||||
              exit $EXIT_CODE
 | 
			
		||||
            fi
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
      - name: Fix HTML Links
 | 
			
		||||
        run: |
 | 
			
		||||
          # Remove .md extensions from links in generated HTML
 | 
			
		||||
          pnpm tsx ./scripts/fix-html-links.ts site
 | 
			
		||||
 | 
			
		||||
      - name: Validate Built Site
 | 
			
		||||
        run: |
 | 
			
		||||
          # Basic validation that important files exist
 | 
			
		||||
          test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
 | 
			
		||||
          test -f site/sitemap.xml || (echo "ERROR: site/sitemap.xml not found" && exit 1)
 | 
			
		||||
          test -d site/assets || (echo "ERROR: site/assets directory not found" && exit 1)
 | 
			
		||||
          echo "✅ Site validation passed"
 | 
			
		||||
 | 
			
		||||
      - name: Deploy
 | 
			
		||||
        uses: ./.github/actions/deploy-to-cloudflare-pages
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -30,7 +30,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: "pnpm"
 | 
			
		||||
      - run: pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								.github/workflows/main-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -46,7 +46,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: "pnpm"
 | 
			
		||||
 | 
			
		||||
      - name: Install npm dependencies
 | 
			
		||||
@@ -86,12 +86,12 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Upload Playwright trace
 | 
			
		||||
        if: failure()
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: Playwright trace (${{ matrix.dockerfile }})
 | 
			
		||||
          path: test-output/playwright/output
 | 
			
		||||
 | 
			
		||||
      - uses: actions/upload-artifact@v5
 | 
			
		||||
      - uses: actions/upload-artifact@v4
 | 
			
		||||
        if: ${{ !cancelled() }}
 | 
			
		||||
        with:
 | 
			
		||||
          name: Playwright report (${{ matrix.dockerfile }})
 | 
			
		||||
@@ -116,10 +116,10 @@ jobs:
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm64
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
          - dockerfile: Dockerfile.legacy
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm/v7
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
          - dockerfile: Dockerfile.legacy
 | 
			
		||||
          - dockerfile: Dockerfile
 | 
			
		||||
            platform: linux/arm/v8
 | 
			
		||||
            image: ubuntu-24.04-arm
 | 
			
		||||
    runs-on: ${{ matrix.image }}
 | 
			
		||||
@@ -146,7 +146,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
@@ -209,7 +209,7 @@ jobs:
 | 
			
		||||
          touch "/tmp/digests/${digest#sha256:}"
 | 
			
		||||
 | 
			
		||||
      - name: Upload digest
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }}
 | 
			
		||||
          path: /tmp/digests/*
 | 
			
		||||
@@ -223,7 +223,7 @@ jobs:
 | 
			
		||||
      - build
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v6
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        with:
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -52,7 +52,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
@@ -89,7 +89,7 @@ jobs:
 | 
			
		||||
          name: Nightly Build
 | 
			
		||||
 | 
			
		||||
      - name: Publish artifacts
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        if: ${{ github.event_name == 'pull_request' }}
 | 
			
		||||
        with:
 | 
			
		||||
          name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/playwright.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
      - uses: pnpm/action-setup@v4
 | 
			
		||||
      - uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
@@ -35,7 +35,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Upload test report
 | 
			
		||||
        if: failure()
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: e2e report
 | 
			
		||||
          path: apps/server-e2e/test-output
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -50,7 +50,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pnpm install --frozen-lockfile
 | 
			
		||||
@@ -73,7 +73,7 @@ jobs:
 | 
			
		||||
          GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload the artifact
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
 | 
			
		||||
          path: apps/desktop/upload/*.*
 | 
			
		||||
@@ -100,7 +100,7 @@ jobs:
 | 
			
		||||
          arch: ${{ matrix.arch }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload the artifact
 | 
			
		||||
        uses: actions/upload-artifact@v5
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: release-server-linux-${{ matrix.arch }}
 | 
			
		||||
          path: upload/*.*
 | 
			
		||||
@@ -120,7 +120,7 @@ jobs:
 | 
			
		||||
            docs/Release Notes
 | 
			
		||||
 | 
			
		||||
      - name: Download all artifacts
 | 
			
		||||
        uses: actions/download-artifact@v6
 | 
			
		||||
        uses: actions/download-artifact@v5
 | 
			
		||||
        with:
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
          pattern: release-*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/website.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -30,7 +30,7 @@ jobs:
 | 
			
		||||
      - name: Set up node & dependencies
 | 
			
		||||
        uses: actions/setup-node@v6
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 24
 | 
			
		||||
          node-version: 22
 | 
			
		||||
          cache: "pnpm"
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
 
 | 
			
		||||
@@ -37,18 +37,20 @@
 | 
			
		||||
  "devDependencies": {    
 | 
			
		||||
    "@playwright/test": "1.56.1",
 | 
			
		||||
    "@stylistic/eslint-plugin": "5.5.0",        
 | 
			
		||||
    "@types/express": "5.0.5",    
 | 
			
		||||
    "@types/node": "24.9.2",    
 | 
			
		||||
    "@types/yargs": "17.0.34",
 | 
			
		||||
    "@types/express": "5.0.3",    
 | 
			
		||||
    "@types/node": "22.18.12",    
 | 
			
		||||
    "@types/yargs": "17.0.33",
 | 
			
		||||
    "@vitest/coverage-v8": "3.2.4",
 | 
			
		||||
    "eslint": "9.39.0",
 | 
			
		||||
    "eslint": "9.38.0",
 | 
			
		||||
    "eslint-plugin-simple-import-sort": "12.1.1",
 | 
			
		||||
    "esm": "3.2.25",
 | 
			
		||||
    "jsdoc": "4.0.5",
 | 
			
		||||
    "lorem-ipsum": "2.0.8",    
 | 
			
		||||
    "rcedit": "4.0.1",
 | 
			
		||||
    "rimraf": "6.1.0",    
 | 
			
		||||
    "tslib": "2.8.1" 
 | 
			
		||||
    "rimraf": "6.0.1",    
 | 
			
		||||
    "tslib": "2.8.1",    
 | 
			
		||||
    "typedoc": "0.28.14",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.1.2"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "appdmg": "0.6.6"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								_regroup/typedoc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/services/backend_script_entrypoint.ts",
 | 
			
		||||
    "src/public/app/services/frontend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ],
 | 
			
		||||
  "outputs": [
 | 
			
		||||
    {
 | 
			
		||||
      "name": "html",
 | 
			
		||||
      "path": "./docs/Script API"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "build-docs",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "main": "src/main.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "tsx ."
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": "Elian Doran <contact@eliandoran.me>",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "packageManager": "pnpm@10.19.0",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@redocly/cli": "2.10.0",
 | 
			
		||||
    "archiver": "7.0.1",
 | 
			
		||||
    "fs-extra": "11.3.2",
 | 
			
		||||
    "react": "19.2.0",
 | 
			
		||||
    "react-dom": "19.2.0",
 | 
			
		||||
    "typedoc": "0.28.14",
 | 
			
		||||
    "typedoc-plugin-missing-exports": "4.1.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The backend script API is accessible to code notes with the "JS (backend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Backend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note on the server side.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with backend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
 | 
			
		||||
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
 | 
			
		||||
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
 | 
			
		||||
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
 | 
			
		||||
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
 | 
			
		||||
export type { BNote };
 | 
			
		||||
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
 | 
			
		||||
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
 | 
			
		||||
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
 | 
			
		||||
 | 
			
		||||
import BNote from "../../server/src/becca/entities/bnote.js";
 | 
			
		||||
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
 | 
			
		||||
 | 
			
		||||
export type { Api };
 | 
			
		||||
 | 
			
		||||
const fakeNote = new BNote();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
 | 
			
		||||
 */
 | 
			
		||||
export const api: Api = new BackendScriptApi(fakeNote, {});
 | 
			
		||||
@@ -1,127 +0,0 @@
 | 
			
		||||
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
 | 
			
		||||
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
 | 
			
		||||
process.env.NODE_ENV = "development";
 | 
			
		||||
 | 
			
		||||
import cls from "@triliumnext/server/src/services/cls.js";
 | 
			
		||||
import { dirname, join, resolve } from "path";
 | 
			
		||||
import * as fs from "fs/promises";
 | 
			
		||||
import * as fsExtra from "fs-extra";
 | 
			
		||||
import archiver from "archiver";
 | 
			
		||||
import { WriteStream } from "fs";
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import BuildContext from "./context.js";
 | 
			
		||||
 | 
			
		||||
const DOCS_ROOT = "../../../docs";
 | 
			
		||||
const OUTPUT_DIR = "../../site";
 | 
			
		||||
 | 
			
		||||
async function buildDocsInner() {
 | 
			
		||||
    const i18n = await import("@triliumnext/server/src/services/i18n.js");
 | 
			
		||||
    await i18n.initializeTranslations();
 | 
			
		||||
 | 
			
		||||
    const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
 | 
			
		||||
    await sqlInit.createInitialDatabase(true);
 | 
			
		||||
 | 
			
		||||
    const note = await importData(join(__dirname, DOCS_ROOT, "User Guide"));
 | 
			
		||||
 | 
			
		||||
    // Export
 | 
			
		||||
    const zipFilePath = "output.zip";
 | 
			
		||||
    try {
 | 
			
		||||
        const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
 | 
			
		||||
        const branch = note.getParentBranches()[0];
 | 
			
		||||
        const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
 | 
			
		||||
            "no-progress-reporting",
 | 
			
		||||
            "export",
 | 
			
		||||
            null
 | 
			
		||||
        );
 | 
			
		||||
        const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
 | 
			
		||||
        await exportToZip(taskContext, branch, "share", fileOutputStream);
 | 
			
		||||
        await waitForStreamToFinish(fileOutputStream);
 | 
			
		||||
        await extractZip(zipFilePath, OUTPUT_DIR);
 | 
			
		||||
    } finally {
 | 
			
		||||
        if (await fsExtra.exists(zipFilePath)) {
 | 
			
		||||
            await fsExtra.rm(zipFilePath);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Copy favicon.
 | 
			
		||||
    await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
 | 
			
		||||
 | 
			
		||||
    console.log("Documentation built successfully!");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function importData(path: string) {
 | 
			
		||||
    const buffer = await createImportZip(path);
 | 
			
		||||
    const importService = (await import("@triliumnext/server/src/services/import/zip.js")).default;
 | 
			
		||||
    const TaskContext = (await import("@triliumnext/server/src/services/task_context.js")).default;
 | 
			
		||||
    const context = new TaskContext("no-progress-reporting", "importNotes", null);
 | 
			
		||||
    const becca = (await import("@triliumnext/server/src/becca/becca.js")).default;
 | 
			
		||||
 | 
			
		||||
    const rootNote = becca.getRoot();
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        throw new Error("Missing root note for import.");
 | 
			
		||||
    }
 | 
			
		||||
    return await importService.importZip(context, buffer, rootNote, {
 | 
			
		||||
        preserveIds: true
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function createImportZip(path: string) {
 | 
			
		||||
    const inputFile = "input.zip";
 | 
			
		||||
    const archive = archiver("zip", {
 | 
			
		||||
        zlib: { level: 0 }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    console.log("Archive path is ", resolve(path))
 | 
			
		||||
    archive.directory(path, "/");
 | 
			
		||||
 | 
			
		||||
    const outputStream = fsExtra.createWriteStream(inputFile);
 | 
			
		||||
    archive.pipe(outputStream);
 | 
			
		||||
    archive.finalize();
 | 
			
		||||
    await waitForStreamToFinish(outputStream);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        return await fsExtra.readFile(inputFile);
 | 
			
		||||
    } finally {
 | 
			
		||||
        await fsExtra.rm(inputFile);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForStreamToFinish(stream: WriteStream) {
 | 
			
		||||
    return new Promise<void>((res, rej) => {
 | 
			
		||||
        stream.on("finish", () => res());
 | 
			
		||||
        stream.on("error", (err) => rej(err));
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
 | 
			
		||||
    const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
 | 
			
		||||
    await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
 | 
			
		||||
        // We ignore directories since they can appear out of order anyway.
 | 
			
		||||
        if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
 | 
			
		||||
            const destPath = join(outputPath, entry.fileName);
 | 
			
		||||
            const fileContent = await readContent(zip, entry);
 | 
			
		||||
 | 
			
		||||
            await fsExtra.mkdirs(dirname(destPath));
 | 
			
		||||
            await fs.writeFile(destPath, fileContent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        zip.readEntry();
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default async function buildDocs({ gitRootDir }: BuildContext) {
 | 
			
		||||
    // Build the share theme.
 | 
			
		||||
    execSync(`pnpm run --filter share-theme build`, {
 | 
			
		||||
        stdio: "inherit",
 | 
			
		||||
        cwd: gitRootDir
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Trigger the actual build.
 | 
			
		||||
    await new Promise((res, rej) => {
 | 
			
		||||
        cls.init(() => {
 | 
			
		||||
            buildDocsInner()
 | 
			
		||||
                .catch(rej)
 | 
			
		||||
                .then(res);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,4 +0,0 @@
 | 
			
		||||
export default interface BuildContext {
 | 
			
		||||
    gitRootDir: string;
 | 
			
		||||
    baseDir: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The front script API is accessible to code notes with the "JS (frontend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Frontend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with frontend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
 | 
			
		||||
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
 | 
			
		||||
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
 | 
			
		||||
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
 | 
			
		||||
export type { default as FNote } from "../../client/src/entities/fnote.js";
 | 
			
		||||
export type { Api } from "../../client/src/services/frontend_script_api.js";
 | 
			
		||||
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
 | 
			
		||||
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
 | 
			
		||||
 | 
			
		||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
 | 
			
		||||
 | 
			
		||||
//@ts-expect-error
 | 
			
		||||
export const api: Api = new FrontendScriptApi();
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import buildSwagger from "./swagger";
 | 
			
		||||
import { existsSync, mkdirSync, rmSync } from "fs";
 | 
			
		||||
import buildDocs from "./build-docs";
 | 
			
		||||
import buildScriptApi from "./script-api";
 | 
			
		||||
 | 
			
		||||
const context: BuildContext = {
 | 
			
		||||
    gitRootDir: join(__dirname, "../../../"),
 | 
			
		||||
    baseDir: join(__dirname, "../../../site")
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
    // Clean input dir.
 | 
			
		||||
    if (existsSync(context.baseDir)) {
 | 
			
		||||
        rmSync(context.baseDir, { recursive: true });
 | 
			
		||||
    }
 | 
			
		||||
    mkdirSync(context.baseDir);
 | 
			
		||||
 | 
			
		||||
    // Start building.
 | 
			
		||||
    await buildDocs(context);
 | 
			
		||||
    buildSwagger(context);
 | 
			
		||||
    buildScriptApi(context);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
 | 
			
		||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
 | 
			
		||||
    // Generate types
 | 
			
		||||
    execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
 | 
			
		||||
 | 
			
		||||
    for (const config of [ "backend", "frontend" ]) {
 | 
			
		||||
        const outDir = join(baseDir, "script-api", config);
 | 
			
		||||
        execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
 | 
			
		||||
            stdio: "inherit"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
import BuildContext from "./context";
 | 
			
		||||
import { join } from "path";
 | 
			
		||||
import { execSync } from "child_process";
 | 
			
		||||
import { mkdirSync } from "fs";
 | 
			
		||||
 | 
			
		||||
interface BuildInfo {
 | 
			
		||||
    specPath: string;
 | 
			
		||||
    outDir: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DIR_PREFIX = "rest-api";
 | 
			
		||||
 | 
			
		||||
const buildInfos: BuildInfo[] = [
 | 
			
		||||
    {
 | 
			
		||||
        // Paths are relative to Git root.
 | 
			
		||||
        specPath: "apps/server/internal.openapi.yaml",
 | 
			
		||||
        outDir: `${DIR_PREFIX}/internal`
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        specPath: "apps/server/etapi.openapi.yaml",
 | 
			
		||||
        outDir: `${DIR_PREFIX}/etapi`
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
 | 
			
		||||
    for (const { specPath, outDir } of buildInfos) {
 | 
			
		||||
        const absSpecPath = join(gitRootDir, specPath);
 | 
			
		||||
        const targetDir = join(baseDir, outDir);
 | 
			
		||||
        mkdirSync(targetDir, { recursive: true });
 | 
			
		||||
        execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../../tsconfig.base.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "target": "ES2020",
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "strict": false,
 | 
			
		||||
    "types": [
 | 
			
		||||
      "node",
 | 
			
		||||
      "express"
 | 
			
		||||
    ],
 | 
			
		||||
    "rootDir": "src",
 | 
			
		||||
    "tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*.ts",
 | 
			
		||||
    "../server/src/*.d.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "exclude": [
 | 
			
		||||
    "eslint.config.js",
 | 
			
		||||
    "eslint.config.cjs",
 | 
			
		||||
    "eslint.config.mjs"
 | 
			
		||||
  ],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../server/tsconfig.app.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../desktop/tsconfig.app.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../client/tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../../tsconfig.base.json",
 | 
			
		||||
  "include": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../server"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "../client"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://typedoc.org/schema.json",
 | 
			
		||||
  "name": "Trilium Backend API",
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/backend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://typedoc.org/schema.json",
 | 
			
		||||
  "name": "Trilium Frontend API",
 | 
			
		||||
  "entryPoints": [
 | 
			
		||||
    "src/frontend_script_entrypoint.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "plugin": [
 | 
			
		||||
    "typedoc-plugin-missing-exports"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
    "circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@eslint/js": "9.39.0",
 | 
			
		||||
    "@eslint/js": "9.38.0",
 | 
			
		||||
    "@excalidraw/excalidraw": "0.18.0",
 | 
			
		||||
    "@fullcalendar/core": "6.1.19",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.19",
 | 
			
		||||
@@ -37,12 +37,12 @@
 | 
			
		||||
    "bootstrap": "5.3.8",
 | 
			
		||||
    "boxicons": "2.1.4",
 | 
			
		||||
    "color": "5.0.2",
 | 
			
		||||
    "dayjs": "1.11.19",
 | 
			
		||||
    "dayjs": "1.11.18",
 | 
			
		||||
    "dayjs-plugin-utc": "0.1.2",
 | 
			
		||||
    "debounce": "2.2.0",
 | 
			
		||||
    "draggabilly": "3.0.0",
 | 
			
		||||
    "force-graph": "1.51.0",
 | 
			
		||||
    "globals": "16.5.0",
 | 
			
		||||
    "globals": "16.4.0",
 | 
			
		||||
    "i18next": "25.6.0",
 | 
			
		||||
    "i18next-http-backend": "3.0.2",
 | 
			
		||||
    "jquery": "3.7.1",
 | 
			
		||||
@@ -54,12 +54,12 @@
 | 
			
		||||
    "leaflet-gpx": "2.2.0",
 | 
			
		||||
    "mark.js": "8.11.1",
 | 
			
		||||
    "marked": "16.4.1",
 | 
			
		||||
    "mermaid": "11.12.1",
 | 
			
		||||
    "mind-elixir": "5.3.4",
 | 
			
		||||
    "mermaid": "11.12.0",
 | 
			
		||||
    "mind-elixir": "5.3.3",
 | 
			
		||||
    "normalize.css": "8.0.1",
 | 
			
		||||
    "panzoom": "9.4.3",
 | 
			
		||||
    "preact": "10.27.2",
 | 
			
		||||
    "react-i18next": "16.2.3",
 | 
			
		||||
    "react-i18next": "16.1.2",
 | 
			
		||||
    "reveal.js": "5.2.1",
 | 
			
		||||
    "svg-pan-zoom": "3.6.2",
 | 
			
		||||
    "tabulator-tables": "6.3.1",
 | 
			
		||||
@@ -74,9 +74,9 @@
 | 
			
		||||
    "@types/leaflet-gpx": "1.3.8",
 | 
			
		||||
    "@types/mark.js": "8.11.12",
 | 
			
		||||
    "@types/reveal.js": "5.2.1",
 | 
			
		||||
    "@types/tabulator-tables": "6.3.0",
 | 
			
		||||
    "@types/tabulator-tables": "6.2.11",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "happy-dom": "20.0.10",
 | 
			
		||||
    "happy-dom": "20.0.7",
 | 
			
		||||
    "script-loader": "0.7.2",
 | 
			
		||||
    "vite-plugin-static-copy": "3.1.4"
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -218,12 +218,12 @@ export type CommandMappings = {
 | 
			
		||||
    /** Works only in the electron context menu. */
 | 
			
		||||
    replaceMisspelling: CommandData;
 | 
			
		||||
 | 
			
		||||
    importMarkdownInline: CommandData;
 | 
			
		||||
    showPasswordNotSet: CommandData;
 | 
			
		||||
    showProtectedSessionPasswordDialog: CommandData;
 | 
			
		||||
    showUploadAttachmentsDialog: CommandData & { noteId: string };
 | 
			
		||||
    showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
 | 
			
		||||
    showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
 | 
			
		||||
    showPasteMarkdownDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
 | 
			
		||||
    closeProtectedSessionPasswordDialog: CommandData;
 | 
			
		||||
    copyImageReferenceToClipboard: CommandData;
 | 
			
		||||
    copyImageToClipboard: CommandData;
 | 
			
		||||
@@ -270,7 +270,6 @@ export type CommandMappings = {
 | 
			
		||||
    closeThisNoteSplit: CommandData;
 | 
			
		||||
    moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
 | 
			
		||||
    jumpToNote: CommandData;
 | 
			
		||||
    openTodayNote: CommandData;
 | 
			
		||||
    commandPalette: CommandData;
 | 
			
		||||
 | 
			
		||||
    // Keyboard shortcuts
 | 
			
		||||
 
 | 
			
		||||
@@ -159,16 +159,6 @@ export default class Entrypoints extends Component {
 | 
			
		||||
        this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async openTodayNoteCommand() {
 | 
			
		||||
        const todayNote = await dateNoteService.getTodayNote();
 | 
			
		||||
        if (!todayNote) {
 | 
			
		||||
            console.warn("Missing today note.");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await appContext.tabManager.openInSameTab(todayNote.noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async runActiveNoteCommand() {
 | 
			
		||||
        const noteContext = appContext.tabManager.getActiveContext();
 | 
			
		||||
        if (!noteContext) {
 | 
			
		||||
 
 | 
			
		||||
@@ -417,7 +417,7 @@ export default class FNote {
 | 
			
		||||
        return notePaths;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] {
 | 
			
		||||
    getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
 | 
			
		||||
        const isHoistedRoot = hoistedNoteId === "root";
 | 
			
		||||
 | 
			
		||||
        const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
 | 
			
		||||
@@ -428,23 +428,7 @@ export default class FNote {
 | 
			
		||||
            isHidden: path.includes("_hidden")
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        // Calculate the length of the prefix match between two arrays
 | 
			
		||||
        const prefixMatchLength = (path: string[], target: string[]) => {
 | 
			
		||||
            const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
 | 
			
		||||
            return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        notePaths.sort((a, b) => {
 | 
			
		||||
            if (activeNotePath) {
 | 
			
		||||
                const activeSegments = activeNotePath.split('/');
 | 
			
		||||
                const aOverlap = prefixMatchLength(a.notePath, activeSegments);
 | 
			
		||||
                const bOverlap = prefixMatchLength(b.notePath, activeSegments);
 | 
			
		||||
                // Paths with more matching prefix segments are prioritized
 | 
			
		||||
                // when the match count is equal, other criteria are used for sorting
 | 
			
		||||
                if (bOverlap !== aOverlap) {
 | 
			
		||||
                    return bOverlap - aOverlap;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
 | 
			
		||||
                return a.isInHoistedSubTree ? -1 : 1;
 | 
			
		||||
            } else if (a.isArchived !== b.isArchived) {
 | 
			
		||||
@@ -465,11 +449,10 @@ export default class FNote {
 | 
			
		||||
     * Returns the note path considered to be the "best"
 | 
			
		||||
     *
 | 
			
		||||
     * @param {string} [hoistedNoteId='root']
 | 
			
		||||
     * @param {string|null} [activeNotePath=null]
 | 
			
		||||
     * @return {string[]} array of noteIds constituting the particular note path
 | 
			
		||||
     */
 | 
			
		||||
    getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) {
 | 
			
		||||
        return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath;
 | 
			
		||||
    getBestNotePath(hoistedNoteId = "root") {
 | 
			
		||||
        return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -56,20 +56,7 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
 | 
			
		||||
                await import("@triliumnext/ckeditor5/src/theme/ck-content.css");
 | 
			
		||||
            }
 | 
			
		||||
            const { $renderedContent } = await content_renderer.getRenderedContent(note, { noChildrenList: true });
 | 
			
		||||
            const container = containerRef.current!;
 | 
			
		||||
            container.replaceChildren(...$renderedContent);
 | 
			
		||||
 | 
			
		||||
            // Wait for all images to load.
 | 
			
		||||
            const images = Array.from(container.querySelectorAll("img"));
 | 
			
		||||
            await Promise.all(
 | 
			
		||||
                images.map(img => {
 | 
			
		||||
                    if (img.complete) return Promise.resolve();
 | 
			
		||||
                    return new Promise<void>(resolve => {
 | 
			
		||||
                        img.addEventListener("load", () => resolve(), { once: true });
 | 
			
		||||
                        img.addEventListener("error", () => resolve(), { once: true });
 | 
			
		||||
                    });
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
            containerRef.current?.replaceChildren(...$renderedContent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        load().then(() => requestAnimationFrame(onReady))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								apps/client/src/services/frontend_script_entrypoint.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,28 @@
 | 
			
		||||
/**
 | 
			
		||||
 * The front script API is accessible to code notes with the "JS (frontend)" language.
 | 
			
		||||
 *
 | 
			
		||||
 * The entire API is exposed as a single global: {@link api}
 | 
			
		||||
 *
 | 
			
		||||
 * @module Frontend Script API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This file creates the entrypoint for TypeDoc that simulates the context from within a
 | 
			
		||||
 * script note.
 | 
			
		||||
 *
 | 
			
		||||
 * Make sure to keep in line with frontend's `script_context.ts`.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type { default as BasicWidget } from "../widgets/basic_widget.js";
 | 
			
		||||
export type { default as FAttachment } from "../entities/fattachment.js";
 | 
			
		||||
export type { default as FAttribute } from "../entities/fattribute.js";
 | 
			
		||||
export type { default as FBranch } from "../entities/fbranch.js";
 | 
			
		||||
export type { default as FNote } from "../entities/fnote.js";
 | 
			
		||||
export type { Api } from "./frontend_script_api.js";
 | 
			
		||||
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
 | 
			
		||||
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
 | 
			
		||||
 | 
			
		||||
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
 | 
			
		||||
 | 
			
		||||
//@ts-expect-error
 | 
			
		||||
export const api: Api = new FrontendScriptApi();
 | 
			
		||||
@@ -20,6 +20,9 @@ function setupGlobs() {
 | 
			
		||||
    window.glob.froca = froca;
 | 
			
		||||
    window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
 | 
			
		||||
 | 
			
		||||
    // for CKEditor integration (button on block toolbar)
 | 
			
		||||
    window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
 | 
			
		||||
 | 
			
		||||
    window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
			
		||||
        const string = String(msg).toLowerCase();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
 | 
			
		||||
    file: null,
 | 
			
		||||
    image: null,
 | 
			
		||||
    launcher: null,
 | 
			
		||||
    mermaid: "s1aBHPd79XYj",
 | 
			
		||||
    mermaid: null,
 | 
			
		||||
    mindMap: null,
 | 
			
		||||
    noteMap: null,
 | 
			
		||||
    relationMap: null,
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,21 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const path = notePath.split("/").reverse();
 | 
			
		||||
 | 
			
		||||
    if (!path.includes("root")) {
 | 
			
		||||
        path.push("root");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const effectivePathSegments: string[] = [];
 | 
			
		||||
    let childNoteId: string | null = null;
 | 
			
		||||
    let i = 0;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < path.length; i++) {
 | 
			
		||||
        const parentNoteId = path[i];
 | 
			
		||||
    while (true) {
 | 
			
		||||
        if (i >= path.length) {
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const parentNoteId = path[i++];
 | 
			
		||||
 | 
			
		||||
        if (childNoteId !== null) {
 | 
			
		||||
            const child = await froca.getNote(childNoteId, !logErrors);
 | 
			
		||||
@@ -56,7 +65,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!parents.some(p => p.noteId === parentNoteId) || (i === path.length - 1 && parentNoteId !== 'root')) {
 | 
			
		||||
            if (!parents.some((p) => p.noteId === parentNoteId)) {
 | 
			
		||||
                if (logErrors) {
 | 
			
		||||
                    const parent = froca.getNoteFromCache(parentNoteId);
 | 
			
		||||
 | 
			
		||||
@@ -68,8 +77,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const activeNotePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
                const bestNotePath = child.getBestNotePath(hoistedNoteId, activeNotePath);
 | 
			
		||||
                const bestNotePath = child.getBestNotePath(hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
                if (bestNotePath) {
 | 
			
		||||
                    const pathToRoot = bestNotePath.reverse().slice(1);
 | 
			
		||||
@@ -100,9 +108,7 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
 | 
			
		||||
        if (!note) {
 | 
			
		||||
            throw new Error(`Unable to find note: ${notePath}.`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const activeNotePath = appContext.tabManager.getActiveContextNotePath();
 | 
			
		||||
        const bestNotePath = note.getBestNotePath(hoistedNoteId, activeNotePath);
 | 
			
		||||
        const bestNotePath = note.getBestNotePath(hoistedNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!bestNotePath) {
 | 
			
		||||
            throw new Error(`Did not find any path segments for '${note.toString()}', hoisted note '${hoistedNoteId}'`);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,11 +11,7 @@ export function reloadFrontendApp(reason?: string) {
 | 
			
		||||
        logInfo(`Frontend app reload: ${reason}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isElectron()) {
 | 
			
		||||
        dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow()?.reload();
 | 
			
		||||
    } else {
 | 
			
		||||
        window.location.reload();
 | 
			
		||||
    }
 | 
			
		||||
    window.location.reload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function restartDesktopApp() {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								apps/client/src/share.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,84 @@
 | 
			
		||||
import "normalize.css";
 | 
			
		||||
import "boxicons/css/boxicons.min.css";
 | 
			
		||||
import "@triliumnext/ckeditor5/src/theme/ck-content.css";
 | 
			
		||||
import "@triliumnext/share-theme/styles/index.css";
 | 
			
		||||
import "@triliumnext/share-theme/scripts/index.js";
 | 
			
		||||
 | 
			
		||||
async function ensureJQuery() {
 | 
			
		||||
    const $ = (await import("jquery")).default;
 | 
			
		||||
    (window as any).$ = $;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function applyMath() {
 | 
			
		||||
    const anyMathBlock = document.querySelector("#content .math-tex");
 | 
			
		||||
    if (!anyMathBlock) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const renderMathInElement = (await import("./services/math.js")).renderMathInElement;
 | 
			
		||||
    renderMathInElement(document.getElementById("content"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function formatCodeBlocks() {
 | 
			
		||||
    const anyCodeBlock = document.querySelector("#content pre");
 | 
			
		||||
    if (!anyCodeBlock) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    await ensureJQuery();
 | 
			
		||||
    const { formatCodeBlocks } = await import("./services/syntax_highlight.js");
 | 
			
		||||
    await formatCodeBlocks($("#content"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setupTextNote() {
 | 
			
		||||
    formatCodeBlocks();
 | 
			
		||||
    applyMath();
 | 
			
		||||
 | 
			
		||||
    const setupMermaid = (await import("./share/mermaid.js")).default;
 | 
			
		||||
    setupMermaid();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fetch note with given ID from backend
 | 
			
		||||
 *
 | 
			
		||||
 * @param noteId of the given note to be fetched. If false, fetches current note.
 | 
			
		||||
 */
 | 
			
		||||
async function fetchNote(noteId: string | null = null) {
 | 
			
		||||
    if (!noteId) {
 | 
			
		||||
        noteId = document.body.getAttribute("data-note-id");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const resp = await fetch(`api/notes/${noteId}`);
 | 
			
		||||
 | 
			
		||||
    return await resp.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener(
 | 
			
		||||
    "DOMContentLoaded",
 | 
			
		||||
    () => {
 | 
			
		||||
        const noteType = determineNoteType();
 | 
			
		||||
 | 
			
		||||
        if (noteType === "text") {
 | 
			
		||||
            setupTextNote();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const toggleMenuButton = document.getElementById("toggleMenuButton");
 | 
			
		||||
        const layout = document.getElementById("layout");
 | 
			
		||||
 | 
			
		||||
        if (toggleMenuButton && layout) {
 | 
			
		||||
            toggleMenuButton.addEventListener("click", () => layout.classList.toggle("showMenu"));
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    false
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function determineNoteType() {
 | 
			
		||||
    const bodyClass = document.body.className;
 | 
			
		||||
    const match = bodyClass.match(/type-([^\s]+)/);
 | 
			
		||||
    return match ? match[1] : null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// workaround to prevent webpack from removing "fetchNote" as dead code:
 | 
			
		||||
// add fetchNote as property to the window object
 | 
			
		||||
Object.defineProperty(window, "fetchNote", {
 | 
			
		||||
    value: fetchNote
 | 
			
		||||
});
 | 
			
		||||
@@ -1,12 +1,7 @@
 | 
			
		||||
export default async function setupMermaid() {
 | 
			
		||||
    const mermaidEls = document.querySelectorAll("#content pre code.language-mermaid");
 | 
			
		||||
    if (mermaidEls.length === 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
import mermaid from "mermaid";
 | 
			
		||||
 | 
			
		||||
    const mermaid = (await import("mermaid")).default;
 | 
			
		||||
 | 
			
		||||
    for (const codeBlock of mermaidEls) {
 | 
			
		||||
export default function setupMermaid() {
 | 
			
		||||
    for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
 | 
			
		||||
        const parentPre = codeBlock.parentElement;
 | 
			
		||||
        if (!parentPre) {
 | 
			
		||||
            continue;
 | 
			
		||||
@@ -2034,9 +2034,9 @@ body.zen #right-pane,
 | 
			
		||||
body.zen #mobile-sidebar-wrapper,
 | 
			
		||||
body.zen .tab-row-container,
 | 
			
		||||
body.zen .tab-row-widget,
 | 
			
		||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
 | 
			
		||||
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
 | 
			
		||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
 | 
			
		||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget.visible)),
 | 
			
		||||
body.zen .ribbon-container:has(.classic-toolbar-widget.visible) .ribbon-top-row,
 | 
			
		||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget.visible)),
 | 
			
		||||
body.zen .note-icon-widget,
 | 
			
		||||
body.zen .title-row .icon-action,
 | 
			
		||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,6 @@
 | 
			
		||||
  "toast": {
 | 
			
		||||
    "critical-error": {
 | 
			
		||||
      "title": "خطأ فادح"
 | 
			
		||||
    },
 | 
			
		||||
    "widget-error": {
 | 
			
		||||
      "title": "فشل في البدء بعنصر الواجهة"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "add_link": {
 | 
			
		||||
@@ -29,8 +26,7 @@
 | 
			
		||||
    "edit_branch_prefix": "تعديل بادئة الفرع",
 | 
			
		||||
    "prefix": "البادئة: ",
 | 
			
		||||
    "save": "حفظ",
 | 
			
		||||
    "help_on_tree_prefix": "مساعدة حول بادئة الشجرة",
 | 
			
		||||
    "branch_prefix_saved": "تم حفظ بادئة الفرع."
 | 
			
		||||
    "help_on_tree_prefix": "مساعدة حول بادئة الشجرة"
 | 
			
		||||
  },
 | 
			
		||||
  "bulk_actions": {
 | 
			
		||||
    "bulk_actions": "اجراءات جماعية",
 | 
			
		||||
@@ -87,8 +83,7 @@
 | 
			
		||||
    "workspace_calendar_root": "تحديد جذر التقويم لكل مساحة عمل",
 | 
			
		||||
    "hide_highlight_widget": "اخفاء عنصر واجهة قائمة التمييزات",
 | 
			
		||||
    "is_owned_by_note": "تخص الملاحظة",
 | 
			
		||||
    "and_more": "... و {{count}}مرات اكثر.",
 | 
			
		||||
    "related_notes_title": "ملاحظات اخرى بنفس التسمية"
 | 
			
		||||
    "and_more": "... و {{count}}مرات اكثر."
 | 
			
		||||
  },
 | 
			
		||||
  "rename_label": {
 | 
			
		||||
    "to": "الى",
 | 
			
		||||
@@ -132,9 +127,7 @@
 | 
			
		||||
    "delete_attachment": "حذف المرفق",
 | 
			
		||||
    "upload_new_revision": "رفع مراجعة جديدة",
 | 
			
		||||
    "copy_link_to_clipboard": "نسخ الرابط الى الحافظة",
 | 
			
		||||
    "convert_attachment_into_note": "تحويل المرفق الى ملاحظة",
 | 
			
		||||
    "delete_success": "تم حذف المرفق \"{{title}}\" .",
 | 
			
		||||
    "enter_new_name": "ادخل اسم مرفق جديد"
 | 
			
		||||
    "convert_attachment_into_note": "تحويل المرفق الى ملاحظة"
 | 
			
		||||
  },
 | 
			
		||||
  "calendar": {
 | 
			
		||||
    "week": "أسبوع",
 | 
			
		||||
@@ -266,8 +259,7 @@
 | 
			
		||||
  "note_paths": {
 | 
			
		||||
    "search": "بحث",
 | 
			
		||||
    "archived": "مؤرشف",
 | 
			
		||||
    "title": "مسارات الملاحظة",
 | 
			
		||||
    "clone_button": "جار نسخ الملاحظة الى مكان جديد..."
 | 
			
		||||
    "title": "مسارات الملاحظة"
 | 
			
		||||
  },
 | 
			
		||||
  "script_executor": {
 | 
			
		||||
    "query": "استعلام",
 | 
			
		||||
@@ -380,8 +372,7 @@
 | 
			
		||||
    "export_note_title": "تصدير الملاحظة",
 | 
			
		||||
    "export_status": "حالة التصدير",
 | 
			
		||||
    "export_finished_successfully": "اكتمل التصدير بنجاح.",
 | 
			
		||||
    "export_in_progress": "جار التصدير: {{progressCount}}",
 | 
			
		||||
    "choose_export_type": "اختر نوع التصدير اولا من فضلك"
 | 
			
		||||
    "export_in_progress": "جار التصدير: {{progressCount}}"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "troubleshooting": "أستكشاف الاخطاء واصلاحها",
 | 
			
		||||
@@ -411,10 +402,7 @@
 | 
			
		||||
    "movingCloningNotes": "نقل/ استنساخ الملاحظات",
 | 
			
		||||
    "deleteNotes": "حذف الملاحظة/ الشجرة الفرعية",
 | 
			
		||||
    "collapseWholeTree": "طي شجرة الملاحظة باكملها",
 | 
			
		||||
    "followLink": "اتبع تلرابط تحت المؤشر",
 | 
			
		||||
    "onlyInDesktop": "في سطح المكتب فقط(Electron build)",
 | 
			
		||||
    "createEditLink": "انشاء/ تحرير رابط خارجي",
 | 
			
		||||
    "quickSearch": "الانتقال الى مربع البحث السريع"
 | 
			
		||||
    "followLink": "اتبع تلرابط تحت المؤشر"
 | 
			
		||||
  },
 | 
			
		||||
  "import": {
 | 
			
		||||
    "options": "خيارات",
 | 
			
		||||
@@ -477,13 +465,7 @@
 | 
			
		||||
    "delete_all_button": "حذف كل المراجعات",
 | 
			
		||||
    "settings": "اعدادات مراجعة الملاحظة",
 | 
			
		||||
    "diff_not_available": "المقارنة غير متوفرة.",
 | 
			
		||||
    "help_title": "مساعدة حول مراجعات الملاحظة",
 | 
			
		||||
    "diff_off_hint": "انقر لعرض محتويات الملاحظة",
 | 
			
		||||
    "revisions_deleted": "تم حذف جميع نسخ المراجعات للملاحظة.",
 | 
			
		||||
    "revision_restored": "تم استعادة نسخ المراجعة للملاحظة.",
 | 
			
		||||
    "revision_deleted": "تم حذف مراجعة الملاحظة.",
 | 
			
		||||
    "snapshot_interval": "فاصل زمني لحفظ لقطات اصدارات المراجعة: {{seconds}}",
 | 
			
		||||
    "maximum_revisions": "حد عدد لقطات اصدارات الملاحظة: {{number}}"
 | 
			
		||||
    "help_title": "مساعدة حول مراجعات الملاحظة"
 | 
			
		||||
  },
 | 
			
		||||
  "sort_child_notes": {
 | 
			
		||||
    "title": "عنوان",
 | 
			
		||||
@@ -497,15 +479,13 @@
 | 
			
		||||
    "sorting_direction": "اتجاه الترتيب",
 | 
			
		||||
    "natural_sort": "الترتيب الطبيعي",
 | 
			
		||||
    "natural_sort_language": "لغات الترتيب الطبيعي",
 | 
			
		||||
    "sort_children_by": "ترتيب العناصر الفرعية حسب...",
 | 
			
		||||
    "sort_folders_at_top": "ترتيب المجلدات في الاعلى"
 | 
			
		||||
    "sort_children_by": "ترتيب العناصر الفرعية حسب..."
 | 
			
		||||
  },
 | 
			
		||||
  "recent_changes": {
 | 
			
		||||
    "undelete_link": "الغاء الحذف",
 | 
			
		||||
    "title": "التغيرات الاخيرة",
 | 
			
		||||
    "no_changes_message": "لايوجد تغيير لحد الان...",
 | 
			
		||||
    "erase_notes_button": "مسح الملاحظات المحذوفة الان",
 | 
			
		||||
    "deleted_notes_message": "تم حذف الملاحظات نهائيا."
 | 
			
		||||
    "erase_notes_button": "مسح الملاحظات المحذوفة الان"
 | 
			
		||||
  },
 | 
			
		||||
  "edited_notes": {
 | 
			
		||||
    "deleted": "(حذف)",
 | 
			
		||||
@@ -716,6 +696,7 @@
 | 
			
		||||
    "backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
 | 
			
		||||
  },
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "wiki": "ويكي",
 | 
			
		||||
    "created": "تم الأنشاء",
 | 
			
		||||
    "actions": "أجراءات",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
@@ -724,9 +705,7 @@
 | 
			
		||||
    "default_token_name": "رمز جديد",
 | 
			
		||||
    "rename_token_title": "اعادة تسمية الرمز",
 | 
			
		||||
    "rename_token": "اعادة تسمية هذا الرمز",
 | 
			
		||||
    "create_token": "انشاء رمز PEAPI جديد",
 | 
			
		||||
    "new_token_title": "رمز ETAPI جديد",
 | 
			
		||||
    "token_created_title": "انشاء رمز ETAPI"
 | 
			
		||||
    "create_token": "انشاء رمز PEAPI جديد"
 | 
			
		||||
  },
 | 
			
		||||
  "password": {
 | 
			
		||||
    "heading": "كلمة المرور",
 | 
			
		||||
@@ -832,8 +811,7 @@
 | 
			
		||||
    "help_on_links": "مساعدة حول الارتباطات التشعبية",
 | 
			
		||||
    "notes_to_clone": "ملاحظات للنسخ",
 | 
			
		||||
    "target_parent_note": "الملاحظة الاصلية الهدف",
 | 
			
		||||
    "clone_to_selected_note": "استنساخ الى الملاحظة المحددة",
 | 
			
		||||
    "no_path_to_clone_to": "لايوجد مسار لنسخ المحتوى الية."
 | 
			
		||||
    "clone_to_selected_note": "استنساخ الى الملاحظة المحددة"
 | 
			
		||||
  },
 | 
			
		||||
  "table_of_contents": {
 | 
			
		||||
    "unit": "عناوين",
 | 
			
		||||
@@ -1051,8 +1029,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "delete_note": {
 | 
			
		||||
    "delete_note": "حذف الملاحظة",
 | 
			
		||||
    "delete_matched_notes": "حف الملاحظات المطابقة",
 | 
			
		||||
    "delete_matched_notes_description": "سوف يؤدي هذا الى حذف الملاحظات المطابقة."
 | 
			
		||||
    "delete_matched_notes": "حف الملاحظات المطابقة"
 | 
			
		||||
  },
 | 
			
		||||
  "rename_note": {
 | 
			
		||||
    "rename_note": "اعادة تسمية الملاحظة",
 | 
			
		||||
@@ -1335,8 +1312,7 @@
 | 
			
		||||
    "notes_to_move": "الملاحظات المراد نقلها",
 | 
			
		||||
    "target_parent_note": "ملاحظة الاصل الهدف",
 | 
			
		||||
    "dialog_title": "انقل الملاحظات الى...",
 | 
			
		||||
    "move_button": "نقل الىالملاحظة المحددة",
 | 
			
		||||
    "error_no_path": "لايوجد مسار لنقل العنصر الية."
 | 
			
		||||
    "move_button": "نقل الىالملاحظة المحددة"
 | 
			
		||||
  },
 | 
			
		||||
  "delete_revisions": {
 | 
			
		||||
    "delete_note_revisions": "حذف مراجعات الملاحظة"
 | 
			
		||||
@@ -1387,8 +1363,7 @@
 | 
			
		||||
    "save_attributes": "حفظ السمات <enter>",
 | 
			
		||||
    "add_a_new_attribute": "اضافة سمة جديدة",
 | 
			
		||||
    "add_new_label_definition": "اضافة تعريف لتسمية جديدة",
 | 
			
		||||
    "add_new_relation_definition": "اضافة تعريف لعلاقة جديدة",
 | 
			
		||||
    "add_new_relation": "اضافة علاقة جديدة <kbd data-command=\"addNewRelation\">"
 | 
			
		||||
    "add_new_relation_definition": "اضافة تعريف لعلاقة جديدة"
 | 
			
		||||
  },
 | 
			
		||||
  "zen_mode": {
 | 
			
		||||
    "button_exit": "الخروج من وضع Zen"
 | 
			
		||||
@@ -1459,8 +1434,5 @@
 | 
			
		||||
  },
 | 
			
		||||
  "png_export_button": {
 | 
			
		||||
    "button_title": "تصدير المخطط كملف PNG"
 | 
			
		||||
  },
 | 
			
		||||
  "protected_session_status": {
 | 
			
		||||
    "inactive": "انقر للدخول الى جلسة محمية"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@
 | 
			
		||||
    "bulk_actions_executed": "批量操作已成功执行。",
 | 
			
		||||
    "none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
 | 
			
		||||
    "labels": "标签",
 | 
			
		||||
    "relations": "关系",
 | 
			
		||||
    "relations": "关联关系",
 | 
			
		||||
    "notes": "笔记",
 | 
			
		||||
    "other": "其它"
 | 
			
		||||
  },
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "导出状态",
 | 
			
		||||
    "export_in_progress": "导出进行中:{{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "导出成功完成。",
 | 
			
		||||
    "format_pdf": "PDF - 用于打印或共享目的。",
 | 
			
		||||
    "share-format": "HTML 网页发布——采用与共享笔记相同的主题,但可发布为静态网站。"
 | 
			
		||||
    "format_pdf": "PDF - 用于打印或共享目的。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "笔记导航",
 | 
			
		||||
@@ -185,8 +184,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "import-status": "导入状态",
 | 
			
		||||
    "in-progress": "导入进行中:{{progress}}",
 | 
			
		||||
    "successful": "导入成功完成。",
 | 
			
		||||
    "importZipRecommendation": "导入 ZIP 文件时,笔记层级将反映压缩文件内的子目录结构。"
 | 
			
		||||
    "successful": "导入成功完成。"
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "包含笔记",
 | 
			
		||||
@@ -261,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "删除此笔记的所有修订版本",
 | 
			
		||||
    "delete_all_button": "删除所有修订版本",
 | 
			
		||||
    "help_title": "关于笔记修订版本的帮助",
 | 
			
		||||
    "revision_last_edited": "此修订版本上次编辑于 {{date}}",
 | 
			
		||||
    "confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
 | 
			
		||||
    "no_revisions": "此笔记暂无修订版本...",
 | 
			
		||||
    "restore_button": "恢复",
 | 
			
		||||
@@ -1289,6 +1288,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI 是一个 REST API,用于以编程方式访问 Trilium 实例,而无需 UI。",
 | 
			
		||||
    "see_more": "有关更多详细信息,请参见 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
 | 
			
		||||
    "wiki": "维基",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI 规范",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "创建新的 ETAPI 令牌",
 | 
			
		||||
    "existing_tokens": "现有令牌",
 | 
			
		||||
    "no_tokens_yet": "目前还没有令牌。点击上面的按钮创建一个。",
 | 
			
		||||
@@ -1555,9 +1558,7 @@
 | 
			
		||||
    "window-on-top": "保持此窗口置顶"
 | 
			
		||||
  },
 | 
			
		||||
  "note_detail": {
 | 
			
		||||
    "could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget",
 | 
			
		||||
    "printing": "正在打印…",
 | 
			
		||||
    "printing_pdf": "正在导出为PDF…"
 | 
			
		||||
    "could_not_find_typewidget": "找不到类型为 '{{type}}' 的 typeWidget"
 | 
			
		||||
  },
 | 
			
		||||
  "note_title": {
 | 
			
		||||
    "placeholder": "请输入笔记标题..."
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    "homepage": "Startseite:",
 | 
			
		||||
    "app_version": "App-Version:",
 | 
			
		||||
    "db_version": "DB-Version:",
 | 
			
		||||
    "sync_version": "Sync-Version:",
 | 
			
		||||
    "sync_version": "Synch-version:",
 | 
			
		||||
    "build_date": "Build-Datum:",
 | 
			
		||||
    "build_revision": "Build-Revision:",
 | 
			
		||||
    "data_directory": "Datenverzeichnis:"
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "Exportstatus",
 | 
			
		||||
    "export_in_progress": "Export läuft: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "Der Export wurde erfolgreich abgeschlossen.",
 | 
			
		||||
    "format_pdf": "PDF - für Ausdrucke oder Teilen.",
 | 
			
		||||
    "share-format": "HTML für die Web-Veröffentlichung – verwendet dasselbe Theme wie bei freigegebenen Notizen, kann jedoch als statische Website veröffentlicht werden."
 | 
			
		||||
    "format_pdf": "PDF - für Ausdrucke oder Teilen."
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "Notiz Navigation",
 | 
			
		||||
@@ -185,8 +184,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "import-status": "Importstatus",
 | 
			
		||||
    "in-progress": "Import läuft: {{progress}}",
 | 
			
		||||
    "successful": "Import erfolgreich abgeschlossen.",
 | 
			
		||||
    "importZipRecommendation": "Beim Import einer ZIP-Datei wird die Notizhierarchie aus der Ordnerstruktur im Archiv übernommen."
 | 
			
		||||
    "successful": "Import erfolgreich abgeschlossen."
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "Notiz beifügen",
 | 
			
		||||
@@ -261,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "Lösche alle Revisionen dieser Notiz",
 | 
			
		||||
    "delete_all_button": "Alle Revisionen löschen",
 | 
			
		||||
    "help_title": "Hilfe zu Notizrevisionen",
 | 
			
		||||
    "revision_last_edited": "Diese Revision wurde zuletzt am {{date}} bearbeitet",
 | 
			
		||||
    "confirm_delete_all": "Möchtest du alle Revisionen dieser Notiz löschen?",
 | 
			
		||||
    "no_revisions": "Für diese Notiz gibt es noch keine Revisionen...",
 | 
			
		||||
    "confirm_restore": "Möchtest du diese Revision wiederherstellen? Dadurch werden der aktuelle Titel und Inhalt der Notiz mit dieser Revision überschrieben.",
 | 
			
		||||
@@ -648,8 +647,7 @@
 | 
			
		||||
    "logout": "Abmelden",
 | 
			
		||||
    "show-cheatsheet": "Cheatsheet anzeigen",
 | 
			
		||||
    "toggle-zen-mode": "Zen Modus",
 | 
			
		||||
    "new-version-available": "Neues Update verfügbar",
 | 
			
		||||
    "download-update": "Version {{latestVersion}} herunterladen"
 | 
			
		||||
    "new-version-available": "Neues Update verfügbar"
 | 
			
		||||
  },
 | 
			
		||||
  "sync_status": {
 | 
			
		||||
    "unknown": "<p>Der Synchronisations-Status wird bekannt, sobald der nächste Synchronisierungsversuch gestartet wird.</p><p>Klicke, um eine Synchronisierung jetzt auszulösen.</p>",
 | 
			
		||||
@@ -991,7 +989,7 @@
 | 
			
		||||
    "enter_password_instruction": "Um die geschützte Notiz anzuzeigen, musst du dein Passwort eingeben:",
 | 
			
		||||
    "start_session_button": "Starte eine geschützte Sitzung <kbd>Eingabetaste</kbd>",
 | 
			
		||||
    "started": "Geschützte Sitzung gestartet.",
 | 
			
		||||
    "wrong_password": "Passwort falsch.",
 | 
			
		||||
    "wrong_password": "Passwort flasch.",
 | 
			
		||||
    "protecting-finished-successfully": "Geschützt erfolgreich beendet.",
 | 
			
		||||
    "unprotecting-finished-successfully": "Ungeschützt erfolgreich beendet.",
 | 
			
		||||
    "protecting-in-progress": "Schützen läuft: {{count}}",
 | 
			
		||||
@@ -1286,6 +1284,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
 | 
			
		||||
    "see_more": "Weitere Details können im {{- link_to_wiki}} und in der {{- link_to_openapi_spec}} oder der {{- link_to_swagger_ui }} gefunden werden.",
 | 
			
		||||
    "wiki": "Wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI-Spezifikation",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Erstelle ein neues ETAPI-Token",
 | 
			
		||||
    "existing_tokens": "Vorhandene Token",
 | 
			
		||||
    "no_tokens_yet": "Es sind noch keine Token vorhanden. Klicke auf die Schaltfläche oben, um eine zu erstellen.",
 | 
			
		||||
@@ -1519,9 +1521,7 @@
 | 
			
		||||
    "window-on-top": "Dieses Fenster immer oben halten"
 | 
			
		||||
  },
 | 
			
		||||
  "note_detail": {
 | 
			
		||||
    "could_not_find_typewidget": "Konnte typeWidget für Typ ‚{{type}}‘ nicht finden",
 | 
			
		||||
    "printing": "Druckvorgang läuft…",
 | 
			
		||||
    "printing_pdf": "PDF-Export läuft…"
 | 
			
		||||
    "could_not_find_typewidget": "Konnte typeWidget für Typ ‚{{type}}‘ nicht finden"
 | 
			
		||||
  },
 | 
			
		||||
  "note_title": {
 | 
			
		||||
    "placeholder": "Titel der Notiz hier eingeben…"
 | 
			
		||||
@@ -1654,7 +1654,7 @@
 | 
			
		||||
    "add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
 | 
			
		||||
    "cut": "Ausschneiden",
 | 
			
		||||
    "copy": "Kopieren",
 | 
			
		||||
    "copy-link": "Link kopieren",
 | 
			
		||||
    "copy-link": "Link opieren",
 | 
			
		||||
    "paste": "Einfügen",
 | 
			
		||||
    "paste-as-plain-text": "Als unformatierten Text einfügen",
 | 
			
		||||
    "search_online": "Suche nach \"{{term}}\" mit {{searchEngine}} starten"
 | 
			
		||||
@@ -2079,7 +2079,6 @@
 | 
			
		||||
  },
 | 
			
		||||
  "presentation_view": {
 | 
			
		||||
    "edit-slide": "Folie bearbeiten",
 | 
			
		||||
    "start-presentation": "Präsentation starten",
 | 
			
		||||
    "slide-overview": "Übersicht der Folien ein-/ausblenden"
 | 
			
		||||
    "start-presentation": "Präsentation starten"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "Export status",
 | 
			
		||||
    "export_in_progress": "Export in progress: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "Export finished successfully.",
 | 
			
		||||
    "format_pdf": "PDF - for printing or sharing purposes.",
 | 
			
		||||
    "share-format": "HTML for web publishing - uses the same theme that is used shared notes, but can be published as a static website."
 | 
			
		||||
    "format_pdf": "PDF - for printing or sharing purposes."
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "title": "Cheatsheet",
 | 
			
		||||
@@ -261,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "Delete all revisions of this note",
 | 
			
		||||
    "delete_all_button": "Delete all revisions",
 | 
			
		||||
    "help_title": "Help on Note Revisions",
 | 
			
		||||
    "revision_last_edited": "This revision was last edited on {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Do you want to delete all revisions of this note?",
 | 
			
		||||
    "no_revisions": "No revisions for this note yet...",
 | 
			
		||||
    "restore_button": "Restore",
 | 
			
		||||
@@ -1453,6 +1453,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI is a REST API used to access Trilium instance programmatically, without UI.",
 | 
			
		||||
    "see_more": "See more details in the {{- link_to_wiki}} and the {{- link_to_openapi_spec}} or the {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI spec",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Create new ETAPI token",
 | 
			
		||||
    "existing_tokens": "Existing tokens",
 | 
			
		||||
    "no_tokens_yet": "There are no tokens yet. Click on the button above to create one.",
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_status": "Estado de exportación",
 | 
			
		||||
    "export_in_progress": "Exportación en curso: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "La exportación finalizó exitosamente.",
 | 
			
		||||
    "format_pdf": "PDF - para propósitos de impresión o compartición.",
 | 
			
		||||
    "share-format": "HTML para publicación web: utiliza el mismo tema que se utiliza en las notas compartidas, pero se puede publicar como un sitio web estático."
 | 
			
		||||
    "format_pdf": "PDF - para propósitos de impresión o compartición."
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "Navegación de notas",
 | 
			
		||||
@@ -185,8 +184,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "import-status": "Estado de importación",
 | 
			
		||||
    "in-progress": "Importación en progreso: {{progress}}",
 | 
			
		||||
    "successful": "Importación finalizada exitosamente.",
 | 
			
		||||
    "importZipRecommendation": "Al importar un archivo ZIP, la jerarquía de notas reflejará la estructura de subdirectorios dentro del archivo comprimido."
 | 
			
		||||
    "successful": "Importación finalizada exitosamente."
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "Incluir nota",
 | 
			
		||||
@@ -261,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "Eliminar todas las revisiones de esta nota",
 | 
			
		||||
    "delete_all_button": "Eliminar todas las revisiones",
 | 
			
		||||
    "help_title": "Ayuda sobre revisiones de notas",
 | 
			
		||||
    "revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
 | 
			
		||||
    "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
 | 
			
		||||
    "no_revisions": "Aún no hay revisiones para esta nota...",
 | 
			
		||||
    "restore_button": "Restaurar",
 | 
			
		||||
@@ -1446,6 +1445,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI es una REST API que se utiliza para acceder a la instancia de Trilium mediante programación, sin interfaz de usuario.",
 | 
			
		||||
    "see_more": "Véa más detalles en el {{- link_to_wiki}} y el {{- link_to_openapi_spec}} o el {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificación ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Crear nuevo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Aún no hay tokens. Dé clic en el botón de arriba para crear uno.",
 | 
			
		||||
@@ -1712,9 +1715,7 @@
 | 
			
		||||
    "window-on-top": "Mantener esta ventana en la parte superior"
 | 
			
		||||
  },
 | 
			
		||||
  "note_detail": {
 | 
			
		||||
    "could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
 | 
			
		||||
    "printing": "Impresión en curso...",
 | 
			
		||||
    "printing_pdf": "Exportando a PDF en curso.."
 | 
			
		||||
    "could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'"
 | 
			
		||||
  },
 | 
			
		||||
  "note_title": {
 | 
			
		||||
    "placeholder": "escriba el título de la nota aquí..."
 | 
			
		||||
 
 | 
			
		||||
@@ -260,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "Supprimer toutes les versions de cette note",
 | 
			
		||||
    "delete_all_button": "Supprimer toutes les versions",
 | 
			
		||||
    "help_title": "Aide sur les versions de notes",
 | 
			
		||||
    "revision_last_edited": "Cette version a été modifiée pour la dernière fois le {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Voulez-vous supprimer toutes les versions de cette note ?",
 | 
			
		||||
    "no_revisions": "Aucune version pour cette note pour l'instant...",
 | 
			
		||||
    "confirm_restore": "Voulez-vous restaurer cette version ? Le titre et le contenu actuels de la note seront écrasés par cette version.",
 | 
			
		||||
@@ -1288,6 +1289,8 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI est une API REST utilisée pour accéder à l'instance Trilium par programme, sans interface utilisateur.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Spec ETAPI OpenAPI",
 | 
			
		||||
    "create_token": "Créer un nouveau jeton ETAPI",
 | 
			
		||||
    "existing_tokens": "Jetons existants",
 | 
			
		||||
    "no_tokens_yet": "Il n'y a pas encore de jetons. Cliquez sur le bouton ci-dessus pour en créer un.",
 | 
			
		||||
@@ -1304,7 +1307,9 @@
 | 
			
		||||
    "delete_token": "Supprimer/désactiver ce token",
 | 
			
		||||
    "rename_token_title": "Renommer le jeton",
 | 
			
		||||
    "rename_token_message": "Veuillez saisir le nom du nouveau jeton",
 | 
			
		||||
    "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?"
 | 
			
		||||
    "delete_token_confirmation": "Êtes-vous sûr de vouloir supprimer le jeton ETAPI « {{name}} » ?",
 | 
			
		||||
    "see_more": "Voir plus de détails dans le {{- link_to_wiki}} et le {{- link_to_openapi_spec}} ou le {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "swagger_ui": "Interface utilisateur ETAPI Swagger"
 | 
			
		||||
  },
 | 
			
		||||
  "options_widget": {
 | 
			
		||||
    "options_status": "Statut des options",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "about": {
 | 
			
		||||
    "title": "ट्रिलियम नोट्स के बारें में"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,50 +1 @@
 | 
			
		||||
{
 | 
			
		||||
  "about": {
 | 
			
		||||
    "title": "A Trilium Notes-ról",
 | 
			
		||||
    "homepage": "Kezdőlap:",
 | 
			
		||||
    "app_version": "Alkalmazás verziója:",
 | 
			
		||||
    "db_version": "Adatbázis verzió:",
 | 
			
		||||
    "sync_version": "Verzió szinkronizálás :",
 | 
			
		||||
    "build_revision": "Build revízió:",
 | 
			
		||||
    "data_directory": "Adatkönyvtár:",
 | 
			
		||||
    "build_date": "Build dátum:"
 | 
			
		||||
  },
 | 
			
		||||
  "toast": {
 | 
			
		||||
    "critical-error": {
 | 
			
		||||
      "title": "Kritikus hiba",
 | 
			
		||||
      "message": "Kritikus hiba történt, amely megakadályozza a kliensalkalmazás indítását:\n\n{{message}}\n\nEzt valószínűleg egy váratlan szkripthiba okozza. Próbálja meg biztonságos módban elindítani az alkalmazást, és hárítsa el a problémát."
 | 
			
		||||
    },
 | 
			
		||||
    "widget-error": {
 | 
			
		||||
      "title": "Nem sikerült inicializálni egy widgetet",
 | 
			
		||||
      "message-custom": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó egyéni widget inicializálása sikertelen volt a következő ok miatt:\n\n{{message}}",
 | 
			
		||||
      "message-unknown": "Ismeretlen widget inicializálása sikertelen volt a következő ok miatt:\n\n{{message}}"
 | 
			
		||||
    },
 | 
			
		||||
    "bundle-error": {
 | 
			
		||||
      "title": "Nem sikerült betölteni az egyéni szkriptet",
 | 
			
		||||
      "message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "add_link": {
 | 
			
		||||
    "add_link": "Link hozzáadása",
 | 
			
		||||
    "help_on_links": "Segítség a linkekhez",
 | 
			
		||||
    "note": "Jegyzet",
 | 
			
		||||
    "search_note": "név szerinti jegyzetkeresés",
 | 
			
		||||
    "link_title_mirrors": "A link cím tükrözi a jegyzet aktuális címét",
 | 
			
		||||
    "link_title_arbitrary": "link cím önkényesen módosítható",
 | 
			
		||||
    "link_title": "Link cím",
 | 
			
		||||
    "button_add_link": "Link hozzáadása"
 | 
			
		||||
  },
 | 
			
		||||
  "branch_prefix": {
 | 
			
		||||
    "edit_branch_prefix": "Az elágazás előtagjának szerkesztése",
 | 
			
		||||
    "help_on_tree_prefix": "Segítség a fa előtagján",
 | 
			
		||||
    "prefix": "Az előtag: ",
 | 
			
		||||
    "save": "Mentés"
 | 
			
		||||
  },
 | 
			
		||||
  "bulk_actions": {
 | 
			
		||||
    "bulk_actions": "Tömeges akciók",
 | 
			
		||||
    "affected_notes": "Érintett jegyzetek",
 | 
			
		||||
    "labels": "Címkék",
 | 
			
		||||
    "relations": "Kapcsolatok",
 | 
			
		||||
    "notes": "Jegyzetek"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
{}
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,10 @@
 | 
			
		||||
    "new_token_message": "Inserisci il nome del nuovo token",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI è un'API REST utilizzata per accedere alle istanze di Trilium in modo programmatico, senza interfaccia utente.",
 | 
			
		||||
    "see_more": "Per maggiori dettagli consulta {{- link_to_wiki}} e {{- link_to_openapi_spec}} o {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Specifiche ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "Interfaccia utente ETAPI Swagger",
 | 
			
		||||
    "create_token": "Crea un nuovo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Token esistenti",
 | 
			
		||||
    "no_tokens_yet": "Non ci sono ancora token. Clicca sul pulsante qui sopra per crearne uno.",
 | 
			
		||||
@@ -863,6 +867,7 @@
 | 
			
		||||
    "delete_all_revisions": "Elimina tutte le revisioni di questa nota",
 | 
			
		||||
    "delete_all_button": "Elimina tutte le revisioni",
 | 
			
		||||
    "help_title": "Aiuto sulle revisioni delle note",
 | 
			
		||||
    "revision_last_edited": "Questa revisione è stata modificata l'ultima volta il {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Vuoi eliminare tutte le revisioni di questa nota?",
 | 
			
		||||
    "no_revisions": "Ancora nessuna revisione per questa nota...",
 | 
			
		||||
    "restore_button": "Ripristina",
 | 
			
		||||
 
 | 
			
		||||
@@ -254,8 +254,7 @@
 | 
			
		||||
    "export_status": "エクスポート状況",
 | 
			
		||||
    "export_in_progress": "エクスポート処理中: {{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "エクスポートが正常に完了しました。",
 | 
			
		||||
    "format_pdf": "PDF - 印刷または共有目的に。",
 | 
			
		||||
    "share-format": "Web 公開用の HTML - 共有ノートで使用されるのと同じテーマを使用しますが、静的 Web サイトとして公開できます。"
 | 
			
		||||
    "format_pdf": "PDF - 印刷または共有目的に。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "title": "チートシート",
 | 
			
		||||
@@ -611,6 +610,7 @@
 | 
			
		||||
    "delete_all_revisions": "このノートの変更履歴をすべて削除",
 | 
			
		||||
    "delete_all_button": "変更履歴をすべて削除",
 | 
			
		||||
    "help_title": "変更履歴のヘルプ",
 | 
			
		||||
    "revision_last_edited": "この変更は{{date}}に行われました",
 | 
			
		||||
    "confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
 | 
			
		||||
    "no_revisions": "このノートに変更履歴はまだありません...",
 | 
			
		||||
    "restore_button": "復元",
 | 
			
		||||
@@ -657,6 +657,10 @@
 | 
			
		||||
    "created": "作成日時",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI は、Trilium インスタンスに UI なしでプログラム的にアクセスするための REST API です。",
 | 
			
		||||
    "see_more": "詳細は{{- link_to_wiki}}と{{- link_to_openapi_spec}}または{{- link_to_swagger_ui }}を参照してください。",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPIの仕様",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "新しくETAPIトークンを作成",
 | 
			
		||||
    "existing_tokens": "既存のトークン",
 | 
			
		||||
    "no_tokens_yet": "トークンはまだありません。上のボタンをクリックして作成してください。",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,6 @@
 | 
			
		||||
    "critical-error": {
 | 
			
		||||
      "title": "Kritische Error",
 | 
			
		||||
      "message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
 | 
			
		||||
    },
 | 
			
		||||
    "widget-error": {
 | 
			
		||||
      "title": "Starten widget mislukt",
 | 
			
		||||
      "message-unknown": "Onbekende widget kan niet gestart worden omdat:\n\n{{message}}"
 | 
			
		||||
    },
 | 
			
		||||
    "bundle-error": {
 | 
			
		||||
      "title": "Custom script laden mislukt"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "add_link": {
 | 
			
		||||
 
 | 
			
		||||
@@ -912,6 +912,7 @@
 | 
			
		||||
    "delete_all_revisions": "Usuń wszystkie wersje tej notatki",
 | 
			
		||||
    "delete_all_button": "Usuń wszystkie wersje",
 | 
			
		||||
    "help_title": "Pomoc dotycząca wersji notatki",
 | 
			
		||||
    "revision_last_edited": "Ta wersja była ostatnio edytowana {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Czy chcesz usunąć wszystkie wersje tej notatki?",
 | 
			
		||||
    "no_revisions": "Brak wersji dla tej notatki...",
 | 
			
		||||
    "restore_button": "Przywróć",
 | 
			
		||||
@@ -1663,6 +1664,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI to interfejs API REST używany do programowego dostępu do instancji Trilium, bez interfejsu użytkownika.",
 | 
			
		||||
    "see_more": "Zobacz więcej szczegółów w {{- link_to_wiki}} oraz w {{- link_to_openapi_spec}} lub {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "specyfikacja ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Utwórz nowy token ETAPI",
 | 
			
		||||
    "existing_tokens": "Istniejące tokeny",
 | 
			
		||||
    "no_tokens_yet": "Nie ma jeszcze żadnych tokenów. Kliknij przycisk powyżej, aby utworzyć jeden.",
 | 
			
		||||
 
 | 
			
		||||
@@ -259,6 +259,7 @@
 | 
			
		||||
    "delete_all_revisions": "Apagar todas as versões desta nota",
 | 
			
		||||
    "delete_all_button": "Apagar todas as versões",
 | 
			
		||||
    "help_title": "Ajuda sobre as versões da nota",
 | 
			
		||||
    "revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Quer apagar todas as versões desta nota?",
 | 
			
		||||
    "no_revisions": "Ainda não há versões para esta nota...",
 | 
			
		||||
    "restore_button": "Recuperar",
 | 
			
		||||
@@ -1422,6 +1423,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI é uma API REST usada para aceder a instância do Trilium programaticamente, sem interface gráfica.",
 | 
			
		||||
    "see_more": "Veja mais pormenores no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificação OpenAPI do ETAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Criar token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
 | 
			
		||||
 
 | 
			
		||||
@@ -415,6 +415,7 @@
 | 
			
		||||
    "delete_all_revisions": "Excluir todas as versões desta nota",
 | 
			
		||||
    "delete_all_button": "Excluir todas as versões",
 | 
			
		||||
    "help_title": "Ajuda sobre as versões da nota",
 | 
			
		||||
    "revision_last_edited": "Esta versão foi editada pela última vez em {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Você quer excluir todas as versões desta nota?",
 | 
			
		||||
    "no_revisions": "Ainda não há versões para esta nota...",
 | 
			
		||||
    "restore_button": "Recuperar",
 | 
			
		||||
@@ -1932,6 +1933,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.",
 | 
			
		||||
    "see_more": "Veja mais detalhes no {{- link_to_wiki}}, na {{- link_to_openapi_spec}} ou na {{- link_to_swagger_ui}}.",
 | 
			
		||||
    "wiki": "wiki",
 | 
			
		||||
    "openapi_spec": "Especificação OpenAPI do ETAPI",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Criar novo token ETAPI",
 | 
			
		||||
    "existing_tokens": "Tokens existentes",
 | 
			
		||||
    "no_tokens_yet": "Ainda não existem tokens. Clique no botão acima para criar um.",
 | 
			
		||||
 
 | 
			
		||||
@@ -507,13 +507,17 @@
 | 
			
		||||
    "new_token_message": "Introduceți denumirea noului token",
 | 
			
		||||
    "new_token_title": "Token ETAPI nou",
 | 
			
		||||
    "no_tokens_yet": "Nu există încă token-uri. Clic pe butonul de deasupra pentru a crea una.",
 | 
			
		||||
    "openapi_spec": "Specificația OpenAPI pentru ETAPI",
 | 
			
		||||
    "swagger_ui": "UI-ul Swagger pentru ETAPI",
 | 
			
		||||
    "rename_token": "Redenumește token-ul",
 | 
			
		||||
    "rename_token_message": "Introduceți denumirea noului token",
 | 
			
		||||
    "rename_token_title": "Redenumire token",
 | 
			
		||||
    "see_more": "Vedeți mai multe detalii în {{- link_to_wiki}} și în {{- link_to_openapi_spec}} sau în {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "token_created_message": "Copiați token-ul creat în clipboard. Trilium stochează token-ul ca hash așadar această valoare poate fi văzută doar acum.",
 | 
			
		||||
    "token_created_title": "Token ETAPI creat",
 | 
			
		||||
    "token_name": "Denumire token"
 | 
			
		||||
    "token_name": "Denumire token",
 | 
			
		||||
    "wiki": "wiki"
 | 
			
		||||
  },
 | 
			
		||||
  "execute_script": {
 | 
			
		||||
    "example_1": "De exemplu, pentru a adăuga un șir de caractere la titlul unei notițe, se poate folosi acest mic script:",
 | 
			
		||||
@@ -1086,6 +1090,7 @@
 | 
			
		||||
    "preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
 | 
			
		||||
    "restore_button": "Restaurează",
 | 
			
		||||
    "revision_deleted": "Revizia notiței a fost ștearsă.",
 | 
			
		||||
    "revision_last_edited": "Revizia a fost ultima oară modificată pe {{date}}",
 | 
			
		||||
    "revision_restored": "Revizia notiței a fost restaurată.",
 | 
			
		||||
    "revisions_deleted": "Notița reviziei a fost ștearsă.",
 | 
			
		||||
    "maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",
 | 
			
		||||
 
 | 
			
		||||
@@ -320,8 +320,7 @@
 | 
			
		||||
    "explodeArchivesTooltip": "Если этот флажок установлен, Trilium будет читать файлы <code>.zip</code>, <code>.enex</code> и <code>.opml</code> и создавать заметки из файлов внутри этих архивов. Если флажок не установлен, Trilium будет прикреплять сами архивы к заметке.",
 | 
			
		||||
    "explodeArchives": "Прочитать содержимое архивов <code>.zip</code>, <code>.enex</code> и <code>.opml</code>.",
 | 
			
		||||
    "shrinkImagesTooltip": "<p>Если этот параметр включен, Trilium попытается уменьшить размер импортируемых изображений путём масштабирования и оптимизации, что может повлиять на воспринимаемое качество изображения. Если этот параметр не установлен, изображения будут импортированы без изменений.</p><p>Это не относится к импорту файлов <code>.zip</code> с метаданными, поскольку предполагается, что эти файлы уже оптимизированы.</p>",
 | 
			
		||||
    "codeImportedAsCode": "Импортировать распознанные файлы кода (например, <code>.json</code>) в виде заметок типа \"код\", если это неясно из метаданных",
 | 
			
		||||
    "importZipRecommendation": "При импорте ZIP файла иерархия заметок будет отражена в структуре папок внутри архива."
 | 
			
		||||
    "codeImportedAsCode": "Импортировать распознанные файлы кода (например, <code>.json</code>) в виде заметок типа \"код\", если это неясно из метаданных"
 | 
			
		||||
  },
 | 
			
		||||
  "markdown_import": {
 | 
			
		||||
    "dialog_title": "Импорт Markdown",
 | 
			
		||||
@@ -366,6 +365,7 @@
 | 
			
		||||
    "delete_all_button": "Удалить все версии",
 | 
			
		||||
    "help_title": "Помощь по версиям заметок",
 | 
			
		||||
    "confirm_delete_all": "Вы хотите удалить все версии этой заметки?",
 | 
			
		||||
    "revision_last_edited": "Эта версия последний раз редактировалась {{date}}",
 | 
			
		||||
    "confirm_restore": "Хотите восстановить эту версию? Текущее название и содержание заметки будут перезаписаны этой версией.",
 | 
			
		||||
    "confirm_delete": "Вы хотите удалить эту версию?",
 | 
			
		||||
    "revisions_deleted": "Версии заметки были удалены.",
 | 
			
		||||
@@ -980,8 +980,7 @@
 | 
			
		||||
    "open_sql_console_history": "Открыть историю консоли SQL",
 | 
			
		||||
    "show_shared_notes_subtree": "Поддерево общедоступных заметок",
 | 
			
		||||
    "switch_to_mobile_version": "Перейти на мобильную версию",
 | 
			
		||||
    "switch_to_desktop_version": "Переключиться на версию для ПК",
 | 
			
		||||
    "new-version-available": "Доступно обновление"
 | 
			
		||||
    "switch_to_desktop_version": "Переключиться на версию для ПК"
 | 
			
		||||
  },
 | 
			
		||||
  "zpetne_odkazy": {
 | 
			
		||||
    "backlink": "{{count}} ссылки",
 | 
			
		||||
@@ -1440,6 +1439,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "wiki": "вики",
 | 
			
		||||
    "created": "Создано",
 | 
			
		||||
    "actions": "Действия",
 | 
			
		||||
    "existing_tokens": "Существующие токены",
 | 
			
		||||
@@ -1447,7 +1447,10 @@
 | 
			
		||||
    "default_token_name": "новый токен",
 | 
			
		||||
    "rename_token_title": "Переименовать токен",
 | 
			
		||||
    "description": "ETAPI — это REST API, используемый для программного доступа к экземпляру Trilium без пользовательского интерфейса.",
 | 
			
		||||
    "see_more": "Более подробную информацию смотрите в {{- link_to_wiki}} и {{- link_to_openapi_spec}} или {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "create_token": "Создать новый токен ETAPI",
 | 
			
		||||
    "openapi_spec": "Спецификация ETAPI OpenAPI",
 | 
			
		||||
    "swagger_ui": "Пользовательский интерфейс ETAPI Swagger",
 | 
			
		||||
    "new_token_title": "Новый токен ETAPI",
 | 
			
		||||
    "token_created_title": "Создан токен ETAPI",
 | 
			
		||||
    "rename_token": "Переименовать этот токен",
 | 
			
		||||
 
 | 
			
		||||
@@ -256,6 +256,7 @@
 | 
			
		||||
        "delete_all_revisions": "Obriši sve revizije ove beleške",
 | 
			
		||||
        "delete_all_button": "Obriši sve revizije",
 | 
			
		||||
        "help_title": "Pomoć za Revizije beleški",
 | 
			
		||||
        "revision_last_edited": "Ova revizija je poslednji put izmenjena {{date}}",
 | 
			
		||||
        "confirm_delete_all": "Da li želite da obrišete sve revizije ove beleške?",
 | 
			
		||||
        "no_revisions": "Još uvek nema revizija za ovu belešku...",
 | 
			
		||||
        "restore_button": "Vrati",
 | 
			
		||||
 
 | 
			
		||||
@@ -104,8 +104,7 @@
 | 
			
		||||
    "export_in_progress": "正在匯出:{{progressCount}}",
 | 
			
		||||
    "export_finished_successfully": "成功匯出。",
 | 
			
		||||
    "format_html": "HTML - 推薦,因為它保留了所有格式",
 | 
			
		||||
    "format_pdf": "PDF - 用於列印或與他人分享。",
 | 
			
		||||
    "share-format": "HTML 網頁發佈——使用與共享筆記相同的佈景主題,但可發佈為靜態網站。"
 | 
			
		||||
    "format_pdf": "PDF - 用於列印或與他人分享。"
 | 
			
		||||
  },
 | 
			
		||||
  "help": {
 | 
			
		||||
    "noteNavigation": "筆記導航",
 | 
			
		||||
@@ -261,6 +260,7 @@
 | 
			
		||||
    "delete_all_revisions": "刪除此筆記的所有歷史版本",
 | 
			
		||||
    "delete_all_button": "刪除所有歷史版本",
 | 
			
		||||
    "help_title": "關於筆記歷史版本的說明",
 | 
			
		||||
    "revision_last_edited": "此歷史版本上次於 {{date}} 編輯",
 | 
			
		||||
    "confirm_delete_all": "您是否要刪除此筆記的所有歷史版本?",
 | 
			
		||||
    "no_revisions": "此筆記暫無歷史版本…",
 | 
			
		||||
    "confirm_restore": "您是否要還原此歷史版本?這將使用此歷史版本覆寫筆記的目前標題和內容。",
 | 
			
		||||
@@ -1281,6 +1281,8 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI 是一個 REST API,用於以編程方式訪問 Trilium 實例,而無需 UI。",
 | 
			
		||||
    "wiki": "維基",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI 規範",
 | 
			
		||||
    "create_token": "新增 ETAPI 令牌",
 | 
			
		||||
    "existing_tokens": "現有令牌",
 | 
			
		||||
    "no_tokens_yet": "目前還沒有令牌。點擊上面的按鈕新增一個。",
 | 
			
		||||
@@ -1297,7 +1299,9 @@
 | 
			
		||||
    "delete_token": "刪除 / 停用此令牌",
 | 
			
		||||
    "rename_token_title": "重新命名令牌",
 | 
			
		||||
    "rename_token_message": "請輸入新的令牌名稱",
 | 
			
		||||
    "delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?"
 | 
			
		||||
    "delete_token_confirmation": "您確定要刪除 ETAPI 令牌 \"{{name}}\" 嗎?",
 | 
			
		||||
    "see_more": "有關更多詳細資訊,請參閱 {{- link_to_wiki}} 和 {{- link_to_openapi_spec}} 或 {{- link_to_swagger_ui}}。",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI"
 | 
			
		||||
  },
 | 
			
		||||
  "options_widget": {
 | 
			
		||||
    "options_status": "選項狀態",
 | 
			
		||||
 
 | 
			
		||||
@@ -309,6 +309,7 @@
 | 
			
		||||
    "delete_all_revisions": "Видалити всі версії цієї нотатки",
 | 
			
		||||
    "delete_all_button": "Видалити всі версії",
 | 
			
		||||
    "help_title": "Довідка щодо Версій нотаток",
 | 
			
		||||
    "revision_last_edited": "Цю версію востаннє редагували {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Ви хочете видалити всі версії цієї нотатки?",
 | 
			
		||||
    "no_revisions": "Поки що немає версій цієї нотатки...",
 | 
			
		||||
    "restore_button": "Відновити",
 | 
			
		||||
@@ -1402,6 +1403,10 @@
 | 
			
		||||
  "etapi": {
 | 
			
		||||
    "title": "ETAPI",
 | 
			
		||||
    "description": "ETAPI — це REST API, який використовується для програмного доступу до екземпляра Trilium без інтерфейсу користувача.",
 | 
			
		||||
    "see_more": "Див. докладнішу інформацію у {{- link_to_wiki}} та {{- link_to_openapi_spec}} або {{- link_to_swagger_ui }}.",
 | 
			
		||||
    "wiki": "вікі",
 | 
			
		||||
    "openapi_spec": "ETAPI OpenAPI spec",
 | 
			
		||||
    "swagger_ui": "ETAPI Swagger UI",
 | 
			
		||||
    "create_token": "Створити новий токен ETAPI",
 | 
			
		||||
    "existing_tokens": "Існуючі токени",
 | 
			
		||||
    "no_tokens_yet": "Токенів поки що немає. Натисніть кнопку вище, щоб створити його.",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								apps/client/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -26,6 +26,7 @@ interface CustomGlobals {
 | 
			
		||||
    appContext: AppContext;
 | 
			
		||||
    froca: Froca;
 | 
			
		||||
    treeCache: Froca;
 | 
			
		||||
    importMarkdownInline: () => Promise<unknown>;
 | 
			
		||||
    SEARCH_HELP_TEXT: string;
 | 
			
		||||
    activeDialog: JQuery<HTMLElement> | null;
 | 
			
		||||
    componentId: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,6 @@ export default function ExportDialog() {
 | 
			
		||||
                        values={[
 | 
			
		||||
                            { value: "html", label: t("export.format_html_zip") },
 | 
			
		||||
                            { value: "markdown", label: t("export.format_markdown") },
 | 
			
		||||
                            { value: "share", label: t("export.share-format") },
 | 
			
		||||
                            { value: "opml", label: t("export.format_opml") }
 | 
			
		||||
                        ]}
 | 
			
		||||
                    />
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import utils from "../../services/utils";
 | 
			
		||||
import Modal from "../react/Modal";
 | 
			
		||||
import Button from "../react/Button";
 | 
			
		||||
import { useTriliumEvent } from "../react/hooks";
 | 
			
		||||
import EditableTextTypeWidget from "../type_widgets/editable_text";
 | 
			
		||||
 | 
			
		||||
interface RenderMarkdownResponse {
 | 
			
		||||
    htmlContent: string;
 | 
			
		||||
@@ -15,34 +14,39 @@ interface RenderMarkdownResponse {
 | 
			
		||||
 | 
			
		||||
export default function MarkdownImportDialog() {
 | 
			
		||||
    const markdownImportTextArea = useRef<HTMLTextAreaElement>(null);
 | 
			
		||||
    const [textTypeWidget, setTextTypeWidget] = useState<EditableTextTypeWidget>();
 | 
			
		||||
    const [ text, setText ] = useState("");
 | 
			
		||||
    const [ shown, setShown ] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useTriliumEvent("showPasteMarkdownDialog", ({ textTypeWidget }) => {
 | 
			
		||||
        setTextTypeWidget(textTypeWidget);
 | 
			
		||||
    const triggerImport = useCallback(() => {
 | 
			
		||||
        if (appContext.tabManager.getActiveContextNoteType() !== "text") {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
        if (utils.isElectron()) {
 | 
			
		||||
            const { clipboard } = utils.dynamicRequire("electron");
 | 
			
		||||
            const text = clipboard.readText();
 | 
			
		||||
    
 | 
			
		||||
            convertMarkdownToHtml(text, textTypeWidget);
 | 
			
		||||
            convertMarkdownToHtml(text);
 | 
			
		||||
        } else {
 | 
			
		||||
            setShown(true);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    useTriliumEvent("importMarkdownInline", triggerImport);
 | 
			
		||||
    useTriliumEvent("pasteMarkdownIntoText", triggerImport);
 | 
			
		||||
 | 
			
		||||
    async function sendForm() {
 | 
			
		||||
        await convertMarkdownToHtml(text);
 | 
			
		||||
        setText("");
 | 
			
		||||
        setShown(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Modal
 | 
			
		||||
            className="markdown-import-dialog" title={t("markdown_import.dialog_title")} size="lg"
 | 
			
		||||
            footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={() => setShown(false)} keyboardShortcut="Ctrl+Enter" />}
 | 
			
		||||
            footer={<Button className="markdown-import-button" text={t("markdown_import.import_button")} onClick={sendForm} keyboardShortcut="Ctrl+Space" />}
 | 
			
		||||
            onShown={() => markdownImportTextArea.current?.focus()}
 | 
			
		||||
            onHidden={async () => {
 | 
			
		||||
                if (textTypeWidget) {
 | 
			
		||||
                    await convertMarkdownToHtml(text, textTypeWidget);
 | 
			
		||||
                }
 | 
			
		||||
                setShown(false);
 | 
			
		||||
                setText("");
 | 
			
		||||
            }}
 | 
			
		||||
            onHidden={() => setShown(false) }
 | 
			
		||||
            show={shown}
 | 
			
		||||
        >
 | 
			
		||||
            <p>{t("markdown_import.modal_body_text")}</p>
 | 
			
		||||
@@ -52,17 +56,26 @@ export default function MarkdownImportDialog() {
 | 
			
		||||
                onKeyDown={(e) => {
 | 
			
		||||
                    if (e.key === "Enter" && e.ctrlKey) {
 | 
			
		||||
                        e.preventDefault();
 | 
			
		||||
                        setShown(false);
 | 
			
		||||
                        sendForm();
 | 
			
		||||
                    }
 | 
			
		||||
                }}></textarea>
 | 
			
		||||
        </Modal>
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function convertMarkdownToHtml(markdownContent: string, textTypeWidget: EditableTextTypeWidget) {
 | 
			
		||||
async function convertMarkdownToHtml(markdownContent: string) {
 | 
			
		||||
    const { htmlContent } = await server.post<RenderMarkdownResponse>("other/render-markdown", { markdownContent });
 | 
			
		||||
 | 
			
		||||
    await textTypeWidget.addHtmlToEditor(htmlContent);
 | 
			
		||||
    
 | 
			
		||||
    const textEditor = await appContext.tabManager.getActiveContext()?.getTextEditor();
 | 
			
		||||
    if (!textEditor) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const viewFragment = textEditor.data.processor.toView(htmlContent);
 | 
			
		||||
    const modelFragment = textEditor.data.toModel(viewFragment);
 | 
			
		||||
 | 
			
		||||
    textEditor.model.insertContent(modelFragment, textEditor.model.document.selection);
 | 
			
		||||
    textEditor.editing.view.focus();
 | 
			
		||||
 | 
			
		||||
    toast.showMessage(t("markdown_import.import_success"));
 | 
			
		||||
}
 | 
			
		||||
@@ -155,11 +155,6 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
 | 
			
		||||
            return Promise.resolve();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Avoid not showing recent notes when creating a new empty tab.
 | 
			
		||||
        if ("noteContext" in data && data.noteContext.ntxId !== "_popup-editor") {
 | 
			
		||||
            return Promise.resolve();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return super.handleEventInChildren(name, data);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -140,10 +140,11 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
 | 
			
		||||
        <FormList onSelect={onSelect} fullHeight>
 | 
			
		||||
            {revisions.map((item) =>
 | 
			
		||||
                <FormListItem
 | 
			
		||||
                    title={t("revisions.revision_last_edited", { date: item.dateLastEdited })}
 | 
			
		||||
                    value={item.revisionId}
 | 
			
		||||
                    active={currentRevision && item.revisionId === currentRevision.revisionId}
 | 
			
		||||
                >
 | 
			
		||||
                    {item.dateCreated && item.dateCreated.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
 | 
			
		||||
                    {item.dateLastEdited && item.dateLastEdited.substr(0, 16)} ({item.contentLength && utils.formatSize(item.contentLength)})
 | 
			
		||||
                </FormListItem>
 | 
			
		||||
            )}
 | 
			
		||||
        </FormList>);
 | 
			
		||||
 
 | 
			
		||||
@@ -147,12 +147,6 @@ const categories: Category[] = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const icons: Icon[] = [
 | 
			
		||||
    {
 | 
			
		||||
        name: "empty",
 | 
			
		||||
        slug: "empty",
 | 
			
		||||
        category_id: 113,
 | 
			
		||||
        type_of_icon: "REGULAR"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: "child",
 | 
			
		||||
        slug: "child-regular",
 | 
			
		||||
 
 | 
			
		||||
@@ -56,16 +56,4 @@
 | 
			
		||||
 | 
			
		||||
.note-icon-widget .icon-list span:hover {
 | 
			
		||||
    border: 1px solid var(--main-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-icon-widget .icon-list span.bx-empty {
 | 
			
		||||
    width: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note-icon-widget .icon-list span.bx-empty::before {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    content: "";
 | 
			
		||||
    border: 1px dashed var(--muted-text-color);
 | 
			
		||||
    width: 1em;
 | 
			
		||||
    height: 1em;
 | 
			
		||||
}
 | 
			
		||||
@@ -264,6 +264,7 @@
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    inset-inline-end: 5px;
 | 
			
		||||
    bottom: 5px;
 | 
			
		||||
    z-index: 1000;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.style-resolver {
 | 
			
		||||
 
 | 
			
		||||
@@ -329,30 +329,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async addHtmlToEditor(html: string) {
 | 
			
		||||
        await this.initialized;
 | 
			
		||||
 | 
			
		||||
        const editor = this.watchdog.editor;
 | 
			
		||||
        if (!editor) return;
 | 
			
		||||
 | 
			
		||||
        editor.model.change((writer) => {
 | 
			
		||||
            const viewFragment = editor.data.processor.toView(html); 
 | 
			
		||||
            const modelFragment = editor.data.toModel(viewFragment); 
 | 
			
		||||
            const insertPosition = editor.model.document.selection.getLastPosition();
 | 
			
		||||
 | 
			
		||||
            if (insertPosition) {
 | 
			
		||||
                const range = editor.model.insertContent(modelFragment, insertPosition);
 | 
			
		||||
 | 
			
		||||
                if (range) {
 | 
			
		||||
                    writer.setSelection(range.end);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        editor.editing.view.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addTextToActiveEditorEvent({ text }: EventData<"addTextToActiveEditor">) {
 | 
			
		||||
        if (!this.isActive()) {
 | 
			
		||||
            return;
 | 
			
		||||
@@ -409,10 +385,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
 | 
			
		||||
        this.triggerCommand("showAddLinkDialog", { textTypeWidget: this, text: selectedText });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pasteMarkdownIntoTextCommand() {
 | 
			
		||||
        this.triggerCommand("showPasteMarkdownDialog", { textTypeWidget: this });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getSelectedText() {
 | 
			
		||||
        const range = this.watchdog.editor?.model.document.selection.getFirstRange();
 | 
			
		||||
        let text = "";
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact";
 | 
			
		||||
import { CSSProperties } from "preact/compat";
 | 
			
		||||
 | 
			
		||||
interface OptionsSectionProps {
 | 
			
		||||
    title?: ComponentChildren;
 | 
			
		||||
    title?: string;
 | 
			
		||||
    children: ComponentChildren;
 | 
			
		||||
    noCard?: boolean;
 | 
			
		||||
    style?: CSSProperties;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ import dialog from "../../../services/dialog";
 | 
			
		||||
import { formatDateTime } from "../../../utils/formatters";
 | 
			
		||||
import ActionButton from "../../react/ActionButton";
 | 
			
		||||
import { useTriliumEvent } from "../../react/hooks";
 | 
			
		||||
import HelpButton from "../../react/HelpButton";
 | 
			
		||||
 | 
			
		||||
type RenameTokenCallback = (tokenId: string, oldName: string) => Promise<void>;
 | 
			
		||||
type DeleteTokenCallback = (tokenId: string, name: string ) => Promise<void>;
 | 
			
		||||
@@ -49,13 +48,19 @@ export default function EtapiSettings() {
 | 
			
		||||
            message: t("etapi.token_created_message"),
 | 
			
		||||
            defaultValue: authToken
 | 
			
		||||
        });
 | 
			
		||||
    }, []);
 | 
			
		||||
    }, []);    
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <OptionsSection title={t("etapi.title")}>
 | 
			
		||||
            <FormText>
 | 
			
		||||
                {t("etapi.description")}
 | 
			
		||||
                <HelpButton helpPage="pgxEVkzLl1OP" />
 | 
			
		||||
                {t("etapi.description")}<br />
 | 
			
		||||
                <RawHtml
 | 
			
		||||
                    html={t("etapi.see_more", {
 | 
			
		||||
                        link_to_wiki: `<a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/etapi.html">${t("etapi.wiki")}</a>`,
 | 
			
		||||
                        // TODO: We use window.open src/public/app/services/link.ts -> prevents regular click behavior on "a" element here because it's a relative path
 | 
			
		||||
                        link_to_openapi_spec: `<a class="tn-link" onclick="window.open('etapi/etapi.openapi.yaml')" href="etapi/etapi.openapi.yaml">${t("etapi.openapi_spec")}</a>`,
 | 
			
		||||
                        link_to_swagger_ui: `<a class="tn-link" href="#_help_f3xpgx6H01PW">${t("etapi.swagger_ui")}</a>`
 | 
			
		||||
                    })} />                    
 | 
			
		||||
            </FormText>
 | 
			
		||||
 | 
			
		||||
            <Button
 | 
			
		||||
@@ -63,7 +68,6 @@ export default function EtapiSettings() {
 | 
			
		||||
                text={t("etapi.create_token")}
 | 
			
		||||
                onClick={createTokenCallback}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <hr />
 | 
			
		||||
 | 
			
		||||
            <h5>{t("etapi.existing_tokens")}</h5>
 | 
			
		||||
@@ -119,7 +123,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
 | 
			
		||||
                                                text={t("etapi.rename_token")}
 | 
			
		||||
                                                onClick={() => renameCallback(etapiTokenId, name)}
 | 
			
		||||
                                            />
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
                                            <ActionButton
 | 
			
		||||
                                                icon="bx bx-trash"
 | 
			
		||||
                                                text={t("etapi.delete_token")}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,7 @@ export default defineConfig(() => ({
 | 
			
		||||
                mobile: join(__dirname, "src", "mobile.ts"),
 | 
			
		||||
                login: join(__dirname, "src", "login.ts"),
 | 
			
		||||
                setup: join(__dirname, "src", "setup.ts"),
 | 
			
		||||
                share: join(__dirname, "src", "share.ts"),
 | 
			
		||||
                set_password: join(__dirname, "src", "set_password.ts"),
 | 
			
		||||
                runtime: join(__dirname, "src", "runtime.ts"),
 | 
			
		||||
                print: join(__dirname, "src", "print.tsx")
 | 
			
		||||
@@ -83,8 +84,7 @@ export default defineConfig(() => ({
 | 
			
		||||
                chunkFileNames: "src/[name].js",
 | 
			
		||||
                assetFileNames: "src/[name].[ext]",
 | 
			
		||||
                manualChunks: {
 | 
			
		||||
                    "ckeditor5": [ "@triliumnext/ckeditor5" ],
 | 
			
		||||
                    "boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
 | 
			
		||||
                    "ckeditor5": [ "@triliumnext/ckeditor5" ]
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            onwarn(warning, rollupWarn) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
 | 
			
		||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
 | 
			
		||||
 | 
			
		||||
:POWERSHELL
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:NODE_TLS_REJECT_UNAUTHORIZED -Value 0; ./trilium.exe"
 | 
			
		||||
GOTO END
 | 
			
		||||
 | 
			
		||||
:BATCH
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
 | 
			
		||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
 | 
			
		||||
 | 
			
		||||
:POWERSHELL
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_DATA_DIR -Value './trilium-data'; ./trilium.exe"
 | 
			
		||||
GOTO END
 | 
			
		||||
 | 
			
		||||
:BATCH
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ WHERE powershell.exe > NUL 2>&1
 | 
			
		||||
IF %ERRORLEVEL% NEQ 0 GOTO BATCH ELSE GOTO POWERSHELL
 | 
			
		||||
 | 
			
		||||
:POWERSHELL
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo -Command "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
 | 
			
		||||
powershell -ExecutionPolicy Bypass -NonInteractive -NoLogo "Set-Item -Path Env:TRILIUM_SAFE_MODE -Value 1; ./trilium.exe --disable-gpu"
 | 
			
		||||
GOTO END
 | 
			
		||||
 | 
			
		||||
:BATCH
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@
 | 
			
		||||
    "@triliumnext/commons": "workspace:*",
 | 
			
		||||
    "@triliumnext/server": "workspace:*",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.3.0",
 | 
			
		||||
    "@electron-forge/cli": "7.10.2",
 | 
			
		||||
    "@electron-forge/maker-deb": "7.10.2",
 | 
			
		||||
    "@electron-forge/maker-dmg": "7.10.2",
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,6 @@ async function main() {
 | 
			
		||||
    // Copy assets.
 | 
			
		||||
    build.copy("src/assets", "assets/");
 | 
			
		||||
    build.copy("/apps/server/src/assets", "assets/");
 | 
			
		||||
    build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
 | 
			
		||||
    build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
 | 
			
		||||
 | 
			
		||||
    // Copy node modules dependencies
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/better-sqlite3": "7.6.13",
 | 
			
		||||
    "@types/mime-types": "3.0.1",
 | 
			
		||||
    "@types/yargs": "17.0.34"
 | 
			
		||||
    "@types/yargs": "17.0.33"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "tsx src/main.ts",
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
    "@triliumnext/desktop": "workspace:*",
 | 
			
		||||
    "@types/fs-extra": "11.0.4",
 | 
			
		||||
    "copy-webpack-plugin": "13.0.1",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.3.0",
 | 
			
		||||
    "fs-extra": "11.3.2"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js
 | 
			
		||||
import debounce from "@triliumnext/client/src/services/debounce.js";
 | 
			
		||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
 | 
			
		||||
import cls from "@triliumnext/server/src/services/cls.js";
 | 
			
		||||
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
 | 
			
		||||
import type { AdvancedExportOptions } from "@triliumnext/server/src/services/export/zip.js";
 | 
			
		||||
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
 | 
			
		||||
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
 | 
			
		||||
 | 
			
		||||
@@ -23,8 +23,6 @@ if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
 | 
			
		||||
    throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BASE_URL = "https://docs.triliumnotes.org";
 | 
			
		||||
 | 
			
		||||
const NOTE_MAPPINGS: NoteMapping[] = [
 | 
			
		||||
    {
 | 
			
		||||
        rootNoteId: "pOsGYCXsbNQG",
 | 
			
		||||
@@ -77,7 +75,7 @@ async function setOptions() {
 | 
			
		||||
    optionsService.setOption("compressImages", "false");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function exportData(noteId: string, format: ExportFormat, outputPath: string, ignoredFiles?: Set<string>) {
 | 
			
		||||
async function exportData(noteId: string, format: "html" | "markdown", outputPath: string, ignoredFiles?: Set<string>) {
 | 
			
		||||
    const zipFilePath = "output.zip";
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
@@ -160,14 +158,6 @@ async function cleanUpMeta(outputPath: string, minify: boolean) {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        el.isExpanded = false;
 | 
			
		||||
 | 
			
		||||
        // Rewrite web view URLs that point to root.
 | 
			
		||||
        if (el.type === "webView" && minify) {
 | 
			
		||||
            const srcAttr = el.attributes.find(attr => attr.name === "webViewSrc");
 | 
			
		||||
            if (srcAttr.value.startsWith("/")) {
 | 
			
		||||
                srcAttr.value = BASE_URL + srcAttr.value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (minify) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-bullseye-slim AS builder
 | 
			
		||||
FROM node:22.20.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-bullseye-slim
 | 
			
		||||
FROM node:22.20.0-bullseye-slim
 | 
			
		||||
# Install only runtime dependencies
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y --no-install-recommends \
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-alpine AS builder
 | 
			
		||||
FROM node:22.20.0-alpine AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-alpine
 | 
			
		||||
FROM node:22.20.0-alpine
 | 
			
		||||
# Install runtime dependencies
 | 
			
		||||
RUN apk add --no-cache su-exec shadow
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-alpine AS builder
 | 
			
		||||
FROM node:22.20.0-alpine AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-alpine
 | 
			
		||||
FROM node:22.20.0-alpine
 | 
			
		||||
# Create a non-root user with configurable UID/GID
 | 
			
		||||
ARG USER=trilium
 | 
			
		||||
ARG UID=1001
 | 
			
		||||
 
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
FROM node:22.21.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
WORKDIR /usr/src/app/build
 | 
			
		||||
COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:22.21.0-bullseye-slim
 | 
			
		||||
# Install only runtime dependencies
 | 
			
		||||
RUN apt-get update && \
 | 
			
		||||
    apt-get install -y --no-install-recommends \
 | 
			
		||||
    gosu && \
 | 
			
		||||
    rm -rf \
 | 
			
		||||
    /var/lib/apt/lists/* \
 | 
			
		||||
    /var/cache/apt/*
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
COPY ./dist /usr/src/app
 | 
			
		||||
RUN rm -rf /usr/src/app/node_modules/better-sqlite3
 | 
			
		||||
COPY --from=builder /usr/src/app/node_modules/better-sqlite3 /usr/src/app/node_modules/better-sqlite3
 | 
			
		||||
COPY ./start-docker.sh /usr/src/app
 | 
			
		||||
 | 
			
		||||
# Configure container
 | 
			
		||||
EXPOSE 8080
 | 
			
		||||
CMD [ "sh", "./start-docker.sh" ]
 | 
			
		||||
HEALTHCHECK --start-period=10s CMD exec gosu node node /usr/src/app/docker_healthcheck.cjs
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:24.11.0-bullseye-slim AS builder
 | 
			
		||||
FROM node:22.20.0-bullseye-slim AS builder
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
# Install native dependencies since we might be building cross-platform.
 | 
			
		||||
@@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
 | 
			
		||||
# We have to use --no-frozen-lockfile due to CKEditor patches
 | 
			
		||||
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
 | 
			
		||||
 | 
			
		||||
FROM node:24.11.0-bullseye-slim
 | 
			
		||||
FROM node:22.20.0-bullseye-slim
 | 
			
		||||
# Create a non-root user with configurable UID/GID
 | 
			
		||||
ARG USER=trilium
 | 
			
		||||
ARG UID=1001
 | 
			
		||||
 
 | 
			
		||||
@@ -26,23 +26,21 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "better-sqlite3": "12.4.1",
 | 
			
		||||
    "html-to-text": "9.0.5",
 | 
			
		||||
    "node-html-parser": "7.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@anthropic-ai/sdk": "0.68.0",
 | 
			
		||||
    "@anthropic-ai/sdk": "0.67.0",
 | 
			
		||||
    "@braintree/sanitize-url": "7.1.1",
 | 
			
		||||
    "@electron/remote": "2.1.3",
 | 
			
		||||
    "@preact/preset-vite": "2.10.2",
 | 
			
		||||
    "@triliumnext/commons": "workspace:*",
 | 
			
		||||
    "@triliumnext/express-partial-content": "workspace:*",
 | 
			
		||||
    "@triliumnext/highlightjs": "workspace:*",
 | 
			
		||||
    "@triliumnext/turndown-plugin-gfm": "workspace:*",
 | 
			
		||||
    "@types/archiver": "7.0.0",
 | 
			
		||||
    "@types/archiver": "6.0.3",
 | 
			
		||||
    "@types/better-sqlite3": "7.6.13",
 | 
			
		||||
    "@types/cls-hooked": "4.3.9",
 | 
			
		||||
    "@types/compression": "1.8.1",
 | 
			
		||||
    "@types/cookie-parser": "1.4.10",
 | 
			
		||||
    "@types/cookie-parser": "1.4.9",
 | 
			
		||||
    "@types/debounce": "1.2.4",
 | 
			
		||||
    "@types/ejs": "3.1.5",
 | 
			
		||||
    "@types/escape-html": "1.0.4",
 | 
			
		||||
@@ -58,37 +56,39 @@
 | 
			
		||||
    "@types/sanitize-html": "2.16.0",
 | 
			
		||||
    "@types/sax": "1.2.7",
 | 
			
		||||
    "@types/serve-favicon": "2.5.7",
 | 
			
		||||
    "@types/serve-static": "2.2.0",
 | 
			
		||||
    "@types/serve-static": "1.15.9",
 | 
			
		||||
    "@types/session-file-store": "1.2.5",
 | 
			
		||||
    "@types/stream-throttle": "0.1.4",
 | 
			
		||||
    "@types/supertest": "6.0.3",
 | 
			
		||||
    "@types/swagger-ui-express": "4.1.8",
 | 
			
		||||
    "@types/tmp": "0.2.6",
 | 
			
		||||
    "@types/turndown": "5.0.6",
 | 
			
		||||
    "@types/turndown": "5.0.5",
 | 
			
		||||
    "@types/ws": "8.18.1",
 | 
			
		||||
    "@types/xml2js": "0.4.14",
 | 
			
		||||
    "archiver": "7.0.1",
 | 
			
		||||
    "async-mutex": "0.5.0",
 | 
			
		||||
    "axios": "1.13.1",
 | 
			
		||||
    "axios": "1.12.2",
 | 
			
		||||
    "bindings": "1.5.0",
 | 
			
		||||
    "bootstrap": "5.3.8",
 | 
			
		||||
    "chardet": "2.1.1",
 | 
			
		||||
    "chardet": "2.1.0",
 | 
			
		||||
    "cheerio": "1.1.2",
 | 
			
		||||
    "chokidar": "4.0.3",
 | 
			
		||||
    "cls-hooked": "4.2.2",
 | 
			
		||||
    "compression": "1.8.1",
 | 
			
		||||
    "cookie-parser": "1.4.7",
 | 
			
		||||
    "csrf-csrf": "3.2.2",
 | 
			
		||||
    "dayjs": "1.11.19",
 | 
			
		||||
    "dayjs": "1.11.18",
 | 
			
		||||
    "debounce": "2.2.0",
 | 
			
		||||
    "debug": "4.4.3",
 | 
			
		||||
    "ejs": "3.1.10",
 | 
			
		||||
    "electron": "38.5.0",
 | 
			
		||||
    "electron": "38.3.0",
 | 
			
		||||
    "electron-debug": "4.1.0",
 | 
			
		||||
    "electron-window-state": "5.0.3",
 | 
			
		||||
    "escape-html": "1.0.3",
 | 
			
		||||
    "express": "5.1.0",
 | 
			
		||||
    "express-http-proxy": "2.1.2",
 | 
			
		||||
    "express-openid-connect": "2.19.2",
 | 
			
		||||
    "express-rate-limit": "8.2.1",
 | 
			
		||||
    "express-rate-limit": "8.1.0",
 | 
			
		||||
    "express-session": "1.18.2",
 | 
			
		||||
    "file-uri-to-path": "2.0.0",
 | 
			
		||||
    "fs-extra": "11.3.2",
 | 
			
		||||
@@ -100,7 +100,7 @@
 | 
			
		||||
    "i18next": "25.6.0",
 | 
			
		||||
    "i18next-fs-backend": "2.6.0",
 | 
			
		||||
    "image-type": "6.0.0",
 | 
			
		||||
    "ini": "6.0.0",
 | 
			
		||||
    "ini": "5.0.0",
 | 
			
		||||
    "is-animated": "2.0.2",
 | 
			
		||||
    "is-svg": "6.1.0",
 | 
			
		||||
    "jimp": "1.6.0",
 | 
			
		||||
@@ -109,8 +109,8 @@
 | 
			
		||||
    "mime-types": "3.0.1",
 | 
			
		||||
    "multer": "2.0.2",
 | 
			
		||||
    "normalize-strings": "1.1.1",
 | 
			
		||||
    "ollama": "0.6.2",
 | 
			
		||||
    "openai": "6.7.0",
 | 
			
		||||
    "ollama": "0.6.0",
 | 
			
		||||
    "openai": "6.6.0",
 | 
			
		||||
    "rand-token": "1.0.1",
 | 
			
		||||
    "safe-compare": "1.1.4",
 | 
			
		||||
    "sanitize-filename": "1.6.3",
 | 
			
		||||
@@ -122,11 +122,12 @@
 | 
			
		||||
    "striptags": "3.2.0",
 | 
			
		||||
    "supertest": "7.1.4",
 | 
			
		||||
    "swagger-jsdoc": "6.2.8",
 | 
			
		||||
    "swagger-ui-express": "5.0.1",
 | 
			
		||||
    "time2fa": "1.4.2",
 | 
			
		||||
    "tmp": "0.2.5",
 | 
			
		||||
    "turndown": "7.2.2",
 | 
			
		||||
    "turndown": "7.2.1",
 | 
			
		||||
    "unescape": "1.0.1",
 | 
			
		||||
    "vite": "7.1.12",
 | 
			
		||||
    "vite": "7.1.11",
 | 
			
		||||
    "ws": "8.18.3",
 | 
			
		||||
    "xml2js": "0.6.2",
 | 
			
		||||
    "yauzl": "3.2.0"
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ async function main() {
 | 
			
		||||
 | 
			
		||||
    // Copy assets
 | 
			
		||||
    build.copy("src/assets", "assets/");
 | 
			
		||||
    build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
 | 
			
		||||
    build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
 | 
			
		||||
 | 
			
		||||
    // Copy node modules dependencies
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
openapi: 3.1.0
 | 
			
		||||
info:
 | 
			
		||||
  title: Internal Trilium API
 | 
			
		||||
  version: 0.99.3
 | 
			
		||||
  title: Trilium Notes Internal API
 | 
			
		||||
  version: 0.98.0
 | 
			
		||||
  description: |
 | 
			
		||||
    This is the internal API used by the Trilium Notes client application.
 | 
			
		||||
    
 | 
			
		||||
@@ -24,12 +24,11 @@ info:
 | 
			
		||||
    State-changing operations require CSRF tokens when using session authentication.
 | 
			
		||||
    
 | 
			
		||||
  contact:
 | 
			
		||||
        name: Trilium Notes Team
 | 
			
		||||
        email: contact@eliandoran.me
 | 
			
		||||
        url: https://triliumnotes.org
 | 
			
		||||
    name: TriliumNext Issue Tracker
 | 
			
		||||
    url: https://github.com/TriliumNext/Trilium/issues
 | 
			
		||||
  license:
 | 
			
		||||
      name: GNU Affero General Public License v3.0 only
 | 
			
		||||
      url: https://www.gnu.org/licenses/agpl-3.0.en.html
 | 
			
		||||
    name: GNU Affero General Public License v3.0
 | 
			
		||||
    url: https://www.gnu.org/licenses/agpl-3.0.html
 | 
			
		||||
    
 | 
			
		||||
servers:
 | 
			
		||||
  - url: http://localhost:8080
 | 
			
		||||
@@ -146,9 +146,228 @@ CREATE INDEX IDX_notes_blobId on notes (blobId);
 | 
			
		||||
CREATE INDEX IDX_revisions_blobId on revisions (blobId);
 | 
			
		||||
CREATE INDEX IDX_attachments_blobId on attachments (blobId);
 | 
			
		||||
 | 
			
		||||
-- Strategic Performance Indexes from migration 234
 | 
			
		||||
-- NOTES TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_notes_search_composite 
 | 
			
		||||
ON notes (isDeleted, type, mime, dateModified DESC);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_notes_metadata_covering 
 | 
			
		||||
ON notes (noteId, isDeleted, type, mime, title, dateModified, isProtected);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_notes_protected_deleted 
 | 
			
		||||
ON notes (isProtected, isDeleted) 
 | 
			
		||||
WHERE isProtected = 1;
 | 
			
		||||
 | 
			
		||||
-- BRANCHES TABLE INDEXES  
 | 
			
		||||
CREATE INDEX IDX_branches_tree_traversal 
 | 
			
		||||
ON branches (parentNoteId, isDeleted, notePosition);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_branches_covering 
 | 
			
		||||
ON branches (noteId, parentNoteId, isDeleted, notePosition, prefix);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_branches_note_parents 
 | 
			
		||||
ON branches (noteId, isDeleted) 
 | 
			
		||||
WHERE isDeleted = 0;
 | 
			
		||||
 | 
			
		||||
-- ATTRIBUTES TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_attributes_search_composite 
 | 
			
		||||
ON attributes (name, value, isDeleted);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_attributes_covering 
 | 
			
		||||
ON attributes (noteId, name, value, type, isDeleted, position);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_attributes_inheritable 
 | 
			
		||||
ON attributes (isInheritable, isDeleted) 
 | 
			
		||||
WHERE isInheritable = 1 AND isDeleted = 0;
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_attributes_labels 
 | 
			
		||||
ON attributes (type, name, value) 
 | 
			
		||||
WHERE type = 'label' AND isDeleted = 0;
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_attributes_relations 
 | 
			
		||||
ON attributes (type, name, value) 
 | 
			
		||||
WHERE type = 'relation' AND isDeleted = 0;
 | 
			
		||||
 | 
			
		||||
-- BLOBS TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_blobs_content_size 
 | 
			
		||||
ON blobs (blobId, LENGTH(content));
 | 
			
		||||
 | 
			
		||||
-- ATTACHMENTS TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_attachments_composite 
 | 
			
		||||
ON attachments (ownerId, role, isDeleted, position);
 | 
			
		||||
 | 
			
		||||
-- REVISIONS TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_revisions_note_date 
 | 
			
		||||
ON revisions (noteId, utcDateCreated DESC);
 | 
			
		||||
 | 
			
		||||
-- ENTITY_CHANGES TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_entity_changes_sync 
 | 
			
		||||
ON entity_changes (isSynced, utcDateChanged);
 | 
			
		||||
 | 
			
		||||
CREATE INDEX IDX_entity_changes_component 
 | 
			
		||||
ON entity_changes (componentId, utcDateChanged DESC);
 | 
			
		||||
 | 
			
		||||
-- RECENT_NOTES TABLE INDEXES
 | 
			
		||||
CREATE INDEX IDX_recent_notes_date 
 | 
			
		||||
ON recent_notes (utcDateCreated DESC);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CREATE TABLE IF NOT EXISTS sessions (
 | 
			
		||||
    id TEXT PRIMARY KEY,
 | 
			
		||||
    data TEXT,
 | 
			
		||||
    expires INTEGER
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- FTS5 Full-Text Search Support
 | 
			
		||||
-- Create FTS5 virtual table with trigram tokenizer
 | 
			
		||||
-- Trigram tokenizer provides language-agnostic substring matching:
 | 
			
		||||
-- 1. Fast substring matching (50-100x speedup for LIKE queries without wildcards)
 | 
			
		||||
-- 2. Case-insensitive search without custom collation
 | 
			
		||||
-- 3. No language-specific stemming assumptions (works for all languages)
 | 
			
		||||
-- 4. Boolean operators (AND, OR, NOT) and phrase matching with quotes
 | 
			
		||||
--
 | 
			
		||||
-- IMPORTANT: Trigram requires minimum 3-character tokens for matching
 | 
			
		||||
-- detail='none' reduces index size by ~50% while maintaining MATCH/rank performance
 | 
			
		||||
-- (loses position info for highlight() function, but snippet() still works)
 | 
			
		||||
CREATE VIRTUAL TABLE notes_fts USING fts5(
 | 
			
		||||
    noteId UNINDEXED,
 | 
			
		||||
    title,
 | 
			
		||||
    content,
 | 
			
		||||
    tokenize = 'trigram',
 | 
			
		||||
    detail = 'none'
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
-- Triggers to keep FTS table synchronized with notes
 | 
			
		||||
-- IMPORTANT: These triggers must handle all SQL operations including:
 | 
			
		||||
-- - Regular INSERT/UPDATE/DELETE
 | 
			
		||||
-- - INSERT OR REPLACE
 | 
			
		||||
-- - INSERT ... ON CONFLICT ... DO UPDATE (upsert)
 | 
			
		||||
-- - Cases where notes are created before blobs (import scenarios)
 | 
			
		||||
 | 
			
		||||
-- Trigger for INSERT operations on notes
 | 
			
		||||
-- Handles: INSERT, INSERT OR REPLACE, INSERT OR IGNORE, and the INSERT part of upsert
 | 
			
		||||
CREATE TRIGGER notes_fts_insert 
 | 
			
		||||
AFTER INSERT ON notes
 | 
			
		||||
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap') 
 | 
			
		||||
    AND NEW.isDeleted = 0
 | 
			
		||||
    AND NEW.isProtected = 0
 | 
			
		||||
BEGIN
 | 
			
		||||
    -- First delete any existing FTS entry (in case of INSERT OR REPLACE)
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = NEW.noteId;
 | 
			
		||||
    
 | 
			
		||||
    -- Then insert the new entry, using LEFT JOIN to handle missing blobs
 | 
			
		||||
    INSERT INTO notes_fts (noteId, title, content)
 | 
			
		||||
    SELECT 
 | 
			
		||||
        NEW.noteId,
 | 
			
		||||
        NEW.title,
 | 
			
		||||
        COALESCE(b.content, '')  -- Use empty string if blob doesn't exist yet
 | 
			
		||||
    FROM (SELECT NEW.noteId) AS note_select
 | 
			
		||||
    LEFT JOIN blobs b ON b.blobId = NEW.blobId;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for UPDATE operations on notes table
 | 
			
		||||
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
 | 
			
		||||
-- Fires for ANY update to searchable notes to ensure FTS stays in sync
 | 
			
		||||
CREATE TRIGGER notes_fts_update 
 | 
			
		||||
AFTER UPDATE ON notes
 | 
			
		||||
WHEN NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
 | 
			
		||||
    -- Fire on any change, not just specific columns, to handle all upsert scenarios
 | 
			
		||||
BEGIN
 | 
			
		||||
    -- Always delete the old entry
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = NEW.noteId;
 | 
			
		||||
    
 | 
			
		||||
    -- Insert new entry if note is not deleted and not protected
 | 
			
		||||
    INSERT INTO notes_fts (noteId, title, content)
 | 
			
		||||
    SELECT 
 | 
			
		||||
        NEW.noteId,
 | 
			
		||||
        NEW.title,
 | 
			
		||||
        COALESCE(b.content, '')  -- Use empty string if blob doesn't exist yet
 | 
			
		||||
    FROM (SELECT NEW.noteId) AS note_select
 | 
			
		||||
    LEFT JOIN blobs b ON b.blobId = NEW.blobId
 | 
			
		||||
    WHERE NEW.isDeleted = 0
 | 
			
		||||
        AND NEW.isProtected = 0;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for UPDATE operations on blobs
 | 
			
		||||
-- Handles: Regular UPDATE and the UPDATE part of upsert (ON CONFLICT DO UPDATE)
 | 
			
		||||
-- IMPORTANT: Uses INSERT OR REPLACE for efficiency with deduplicated blobs
 | 
			
		||||
CREATE TRIGGER notes_fts_blob_update 
 | 
			
		||||
AFTER UPDATE ON blobs
 | 
			
		||||
BEGIN
 | 
			
		||||
    -- Use INSERT OR REPLACE for atomic update of all notes sharing this blob
 | 
			
		||||
    -- This is more efficient than DELETE + INSERT when many notes share the same blob
 | 
			
		||||
    INSERT OR REPLACE INTO notes_fts (noteId, title, content)
 | 
			
		||||
    SELECT 
 | 
			
		||||
        n.noteId,
 | 
			
		||||
        n.title,
 | 
			
		||||
        NEW.content
 | 
			
		||||
    FROM notes n
 | 
			
		||||
    WHERE n.blobId = NEW.blobId
 | 
			
		||||
        AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
 | 
			
		||||
        AND n.isDeleted = 0
 | 
			
		||||
        AND n.isProtected = 0;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for DELETE operations
 | 
			
		||||
CREATE TRIGGER notes_fts_delete 
 | 
			
		||||
AFTER DELETE ON notes
 | 
			
		||||
BEGIN
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = OLD.noteId;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for soft delete (isDeleted = 1)
 | 
			
		||||
CREATE TRIGGER notes_fts_soft_delete 
 | 
			
		||||
AFTER UPDATE ON notes
 | 
			
		||||
WHEN OLD.isDeleted = 0 AND NEW.isDeleted = 1
 | 
			
		||||
BEGIN
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = NEW.noteId;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for notes becoming protected
 | 
			
		||||
-- Remove from FTS when a note becomes protected
 | 
			
		||||
CREATE TRIGGER notes_fts_protect 
 | 
			
		||||
AFTER UPDATE ON notes
 | 
			
		||||
WHEN OLD.isProtected = 0 AND NEW.isProtected = 1
 | 
			
		||||
BEGIN
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = NEW.noteId;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for notes becoming unprotected
 | 
			
		||||
-- Add to FTS when a note becomes unprotected (if eligible)
 | 
			
		||||
CREATE TRIGGER notes_fts_unprotect 
 | 
			
		||||
AFTER UPDATE ON notes
 | 
			
		||||
WHEN OLD.isProtected = 1 AND NEW.isProtected = 0
 | 
			
		||||
    AND NEW.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
 | 
			
		||||
    AND NEW.isDeleted = 0
 | 
			
		||||
BEGIN
 | 
			
		||||
    DELETE FROM notes_fts WHERE noteId = NEW.noteId;
 | 
			
		||||
    
 | 
			
		||||
    INSERT INTO notes_fts (noteId, title, content)
 | 
			
		||||
    SELECT 
 | 
			
		||||
        NEW.noteId,
 | 
			
		||||
        NEW.title,
 | 
			
		||||
        COALESCE(b.content, '')
 | 
			
		||||
    FROM (SELECT NEW.noteId) AS note_select
 | 
			
		||||
    LEFT JOIN blobs b ON b.blobId = NEW.blobId;
 | 
			
		||||
END;
 | 
			
		||||
 | 
			
		||||
-- Trigger for INSERT operations on blobs
 | 
			
		||||
-- Handles: INSERT, INSERT OR REPLACE, and the INSERT part of upsert
 | 
			
		||||
-- Updates all notes that reference this blob (common during import and deduplication)
 | 
			
		||||
CREATE TRIGGER notes_fts_blob_insert 
 | 
			
		||||
AFTER INSERT ON blobs
 | 
			
		||||
BEGIN
 | 
			
		||||
    -- Use INSERT OR REPLACE to handle both new and existing FTS entries
 | 
			
		||||
    -- This is crucial for blob deduplication where multiple notes may already
 | 
			
		||||
    -- exist that reference this blob before the blob itself is created
 | 
			
		||||
    INSERT OR REPLACE INTO notes_fts (noteId, title, content)
 | 
			
		||||
    SELECT 
 | 
			
		||||
        n.noteId,
 | 
			
		||||
        n.title,
 | 
			
		||||
        NEW.content
 | 
			
		||||
    FROM notes n
 | 
			
		||||
    WHERE n.blobId = NEW.blobId
 | 
			
		||||
        AND n.type IN ('text', 'code', 'mermaid', 'canvas', 'mindMap')
 | 
			
		||||
        AND n.isDeleted = 0
 | 
			
		||||
        AND n.isProtected = 0;
 | 
			
		||||
END;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						| 
		 Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB  | 
| 
		 Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB  | 
| 
		 Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB  | 
| 
		 Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB  | 
| 
		 Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 167 KiB  | 
| 
		 Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB  | 
| 
		 Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 202 KiB  | 
| 
		 Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB  | 
| 
		 Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB  | 
| 
		 Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 191 KiB  | 
@@ -11,12 +11,12 @@
 | 
			
		||||
<p>To set your preferred chat model, you'll want to enter the provider's
 | 
			
		||||
  name here:</p>
 | 
			
		||||
<figure class="image image_resized" style="width:88.38%;">
 | 
			
		||||
  <img style="aspect-ratio:1884/1267;" src="Providers_image.png"
 | 
			
		||||
  <img style="aspect-ratio:1884/1267;" src="AI Provider Information_im.png"
 | 
			
		||||
  width="1884" height="1267">
 | 
			
		||||
</figure>
 | 
			
		||||
<p>And to set your preferred embedding provider:</p>
 | 
			
		||||
<figure class="image image_resized"
 | 
			
		||||
style="width:93.47%;">
 | 
			
		||||
  <img style="aspect-ratio:1907/1002;" src="1_Providers_image.png"
 | 
			
		||||
  <img style="aspect-ratio:1907/1002;" src="1_AI Provider Information_im.png"
 | 
			
		||||
  width="1907" height="1002">
 | 
			
		||||
</figure>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB  | 
| 
		 Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB  |