Compare commits

..

1 Commits

Author SHA1 Message Date
perf3ct
c0a55fec60 idk 2025-07-12 00:33:25 +00:00
1951 changed files with 48555 additions and 209648 deletions

View File

@@ -1,6 +1,6 @@
root = true root = true
[*.{js,ts,tsx}] [*.{js,ts}]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 4 indent_size = 4

1
.env Normal file
View File

@@ -0,0 +1 @@
NODE_OPTIONS=--max_old_space_size=4096

2
.github/FUNDING.yml vendored
View File

@@ -2,5 +2,3 @@
github: [eliandoran] github: [eliandoran]
custom: ["https://paypal.me/eliandoran"] custom: ["https://paypal.me/eliandoran"]
liberapay: ElianDoran
buy_me_a_coffee: eliandoran

View File

@@ -74,7 +74,7 @@ runs:
- name: Update build info - name: Update build info
shell: ${{ inputs.shell }} shell: ${{ inputs.shell }}
run: pnpm run chore:update-build-info run: npm run chore:update-build-info
# Critical debugging configuration # Critical debugging configuration
- name: Run electron-forge build with enhanced logging - name: Run electron-forge build with enhanced logging
@@ -86,8 +86,7 @@ runs:
APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ env.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ env.WINDOWS_SIGN_EXECUTABLE }}
TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
TARGET_ARCH: ${{ inputs.arch }} run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
run: pnpm run --filter desktop electron-forge:make --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
# Add DMG signing step # Add DMG signing step
- name: Sign DMG - name: Sign DMG
@@ -163,25 +162,3 @@ runs:
echo "Found ZIP: $zip_file" echo "Found ZIP: $zip_file"
echo "Note: ZIP files are not code signed, but their contents should be" echo "Note: ZIP files are not code signed, but their contents should be"
fi fi
- name: Sign the RPM
if: inputs.os == 'linux'
shell: ${{ inputs.shell }}
run: |
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
# Import the key into RPM for verification
gpg --export -a > pubkey
rpm --import pubkey
rm pubkey
# Sign the RPM
rpm_file=$(find ./apps/desktop/upload -name "*.rpm" -print -quit)
rpmsign --define "_gpg_name Trilium Notes Signing Key <triliumnotes@outlook.com>" --addsign "$rpm_file"
rpm -Kv "$rpm_file"
# Validate code signing
if ! rpm -K "$rpm_file" | grep -q "digests signatures OK"; then
echo .rpm file not signed
exit 1
fi

View File

@@ -10,9 +10,9 @@ runs:
steps: steps:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: "pnpm" cache: "pnpm"
- name: Install dependencies - name: Install dependencies
shell: bash shell: bash
@@ -23,7 +23,7 @@ runs:
shell: bash shell: bash
run: | run: |
pnpm run chore:update-build-info pnpm run chore:update-build-info
pnpm run --filter server package pnpm nx --project=server package
- name: Prepare artifacts - name: Prepare artifacts
shell: bash shell: bash
run: | run: |

View File

@@ -1,103 +0,0 @@
name: "Deploy to Cloudflare Pages"
description: "Deploys to Cloudflare Pages on either a temporary branch with preview comment, or on the production version if on the main branch."
inputs:
project_name:
description: "CloudFlare Pages project name"
comment_body:
description: "The message to display when deployment is ready"
default: "Deployment is ready."
required: false
production_url:
description: "The URL to mention as the production URL."
required: true
deploy_dir:
description: "The directory from which to deploy."
required: true
cloudflare_api_token:
description: "The Cloudflare API token to use for deployment."
required: true
cloudflare_account_id:
description: "The Cloudflare account ID to use for deployment."
required: true
github_token:
description: "The GitHub token to use for posting PR comments."
required: true
runs:
using: composite
steps:
# Install wrangler globally to avoid workspace issues
- name: Install Wrangler
shell: bash
run: npm install -g wrangler
# Deploy using Wrangler (use pre-installed wrangler)
- name: Deploy to Cloudflare Pages
id: deploy
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ inputs.cloudflare_api_token }}
accountId: ${{ inputs.cloudflare_account_id }}
command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=${{ github.ref_name }}
wranglerVersion: '' # Use pre-installed version
# Deploy preview for PRs
- name: Deploy Preview to Cloudflare Pages
id: preview-deployment
if: github.event_name == 'pull_request'
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ inputs.cloudflare_api_token }}
accountId: ${{ inputs.cloudflare_account_id }}
command: pages deploy ${{ inputs.deploy_dir }} --project-name=${{ inputs.project_name}} --branch=pr-${{ github.event.pull_request.number }}
wranglerVersion: '' # Use pre-installed version
# Post deployment URL as PR comment
- name: Comment PR with Preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v8
env:
COMMENT_BODY: ${{ inputs.comment_body }}
PRODUCTION_URL: ${{ inputs.production_url }}
PROJECT_NAME: ${{ inputs.project_name }}
with:
github-token: ${{ inputs.github_token }}
script: |
const prNumber = context.issue.number;
// Construct preview URL based on Cloudflare Pages pattern
const projectName = process.env.PROJECT_NAME;
const previewUrl = `https://pr-${prNumber}.${projectName}.pages.dev`;
// Check if we already commented
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const customMessage = process.env.COMMENT_BODY;
const botComment = comments.data.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes(customMessage)
);
const mainUrl = process.env.PRODUCTION_URL;
const commentBody = `${customMessage}!\n\n🔗 Preview URL: ${previewUrl}\n📖 Production URL: ${mainUrl}\n\n✅ All checks passed\n\n_This preview will be updated automatically with new commits._`;
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: prNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody
});
}

View File

@@ -44,7 +44,7 @@ runs:
steps: steps:
# Checkout branch to compare to [required] # Checkout branch to compare to [required]
- name: Checkout base branch - name: Checkout base branch
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
path: br-base path: br-base

View File

@@ -1,18 +0,0 @@
name: Checks
on:
push:
pull_request_target:
types: [synchronize]
jobs:
main:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check if PRs have conflicts
uses: eps1lon/actions-label-merge-conflict@v3
if: github.repository == ${{ vars.REPO_MAIN }}
with:
dirtyLabel: "merge-conflicts"
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"

View File

@@ -57,7 +57,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action. # Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node` # This includes steps like installing compilers or runtimes (`actions/setup-node`
@@ -67,7 +67,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -95,6 +95,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -1,78 +0,0 @@
name: Deploy Documentation
on:
# Trigger on push to main branch
push:
branches:
- main
- master # Also support master branch
# Only run when docs files change
paths:
- 'docs/**'
- 'apps/edit-docs/**'
- 'apps/build-docs/**'
- 'packages/share-theme/**'
# Allow manual triggering from Actions tab
workflow_dispatch:
# Run on pull requests for preview deployments
pull_request:
branches:
- main
- master
paths:
- 'docs/**'
- 'apps/edit-docs/**'
- 'apps/build-docs/**'
- 'packages/share-theme/**'
jobs:
build-and-deploy:
name: Build and Deploy Documentation
runs-on: ubuntu-latest
timeout-minutes: 10
# Required permissions for deployment
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
id-token: write # For OIDC authentication (if needed)
steps:
- name: Checkout Repository
uses: actions/checkout@v5
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Trigger build of documentation
run: pnpm docs:build
- name: Validate Built Site
run: |
test -f site/index.html || (echo "ERROR: site/index.html not found" && exit 1)
test -f site/developer-guide/index.html || (echo "ERROR: site/developer-guide/index.html not found" && exit 1)
echo "✓ User Guide and Developer Guide built successfully"
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
if: github.repository == ${{ vars.REPO_MAIN }}
with:
project_name: "trilium-docs"
comment_body: "📚 Documentation preview is ready"
production_url: "https://docs.triliumnotes.org"
deploy_dir: "site"
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -19,24 +19,45 @@ permissions:
pull-requests: write # for PR comments pull-requests: write # for PR comments
jobs: jobs:
test_dev: check-affected:
name: Test development name: Check affected jobs (NX)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for https://github.com/marketplace/actions/nx-set-shas
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- name: Check affected
run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build
test_dev:
name: Test development
runs-on: ubuntu-latest
needs:
- check-affected
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 22
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Run the unit tests - name: Run the unit tests
run: pnpm run test:all run: pnpm run test:all
@@ -45,15 +66,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- test_dev - test_dev
- check-affected
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Update build info - name: Update build info
run: pnpm run chore:update-build-info run: pnpm run chore:update-build-info
- name: Trigger client build - name: Trigger client build
run: pnpm client:build run: pnpm nx run client:build
- name: Send client bundle stats to RelativeCI - name: Send client bundle stats to RelativeCI
if: false if: false
uses: relative-ci/agent-action@v3 uses: relative-ci/agent-action@v3
@@ -61,7 +83,7 @@ jobs:
webpackStatsFile: ./apps/client/dist/webpack-stats.json webpackStatsFile: ./apps/client/dist/webpack-stats.json
key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }} key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }}
- name: Trigger server build - name: Trigger server build
run: pnpm run server:build run: pnpm nx run server:build
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6 - uses: docker/build-push-action@v6
with: with:
@@ -73,6 +95,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build_docker - build_docker
- check-affected
strategy: strategy:
matrix: matrix:
include: include:
@@ -80,7 +103,7 @@ jobs:
- dockerfile: Dockerfile - dockerfile: Dockerfile
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v5 uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Install dependencies - name: Install dependencies
@@ -89,7 +112,7 @@ jobs:
- name: Update build info - name: Update build info
run: pnpm run chore:update-build-info run: pnpm run chore:update-build-info
- name: Trigger build - name: Trigger build
run: pnpm server:build run: pnpm nx run server:build
- name: Set IMAGE_NAME to lowercase - name: Set IMAGE_NAME to lowercase
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV

View File

@@ -32,7 +32,7 @@ jobs:
- dockerfile: Dockerfile - dockerfile: Dockerfile
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v5 uses: actions/checkout@v4
- name: Set IMAGE_NAME to lowercase - name: Set IMAGE_NAME to lowercase
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
@@ -44,9 +44,9 @@ jobs:
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: "pnpm" cache: "pnpm"
- name: Install npm dependencies - name: Install npm dependencies
@@ -82,16 +82,16 @@ jobs:
require-healthy: true require-healthy: true
- name: Run Playwright tests - name: Run Playwright tests
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm --filter=server-e2e e2e run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
- name: Upload Playwright trace - name: Upload Playwright trace
if: failure() if: failure()
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: Playwright trace (${{ matrix.dockerfile }}) name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output path: test-output/playwright/output
- uses: actions/upload-artifact@v5 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
name: Playwright report (${{ matrix.dockerfile }}) name: Playwright report (${{ matrix.dockerfile }})
@@ -116,10 +116,10 @@ jobs:
- dockerfile: Dockerfile - dockerfile: Dockerfile
platform: linux/arm64 platform: linux/arm64
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
- dockerfile: Dockerfile.legacy - dockerfile: Dockerfile
platform: linux/arm/v7 platform: linux/arm/v7
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
- dockerfile: Dockerfile.legacy - dockerfile: Dockerfile
platform: linux/arm/v8 platform: linux/arm/v8
image: ubuntu-24.04-arm image: ubuntu-24.04-arm
runs-on: ${{ matrix.image }} runs-on: ${{ matrix.image }}
@@ -141,23 +141,23 @@ jobs:
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Update build info
run: pnpm run chore:update-build-info
- name: Run the TypeScript build - name: Run the TypeScript build
run: pnpm run server:build run: pnpm run server:build
- name: Update build info
run: pnpm run chore:update-build-info
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -209,9 +209,9 @@ jobs:
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: digests-${{ env.PLATFORM_PAIR }}-${{ matrix.dockerfile }} name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/* path: /tmp/digests/*
if-no-files-found: error if-no-files-found: error
retention-days: 1 retention-days: 1
@@ -223,7 +223,7 @@ jobs:
- build - build
steps: steps:
- name: Download digests - name: Download digests
uses: actions/download-artifact@v6 uses: actions/download-artifact@v4
with: with:
path: /tmp/digests path: /tmp/digests
pattern: digests-* pattern: digests-*

View File

@@ -19,6 +19,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label}
GITHUB_RELEASE_ID: 179589950 GITHUB_RELEASE_ID: 179589950
permissions: permissions:
@@ -26,7 +27,6 @@ permissions:
jobs: jobs:
nightly-electron: nightly-electron:
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy nightly name: Deploy nightly
strategy: strategy:
fail-fast: false fail-fast: false
@@ -47,15 +47,16 @@ jobs:
forge_platform: win32 forge_platform: win32
runs-on: ${{ matrix.os.image }} runs-on: ${{ matrix.os.image }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- name: Update nightly version - name: Update nightly version
run: npm run chore:ci-update-nightly-version run: npm run chore:ci-update-nightly-version
- name: Run the build - name: Run the build
@@ -74,10 +75,9 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2.4.2 uses: softprops/action-gh-release@v2.3.2
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
with: with:
make_latest: false make_latest: false
@@ -89,14 +89,13 @@ jobs:
name: Nightly Build name: Nightly Build
- name: Publish artifacts - name: Publish artifacts
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
with: with:
name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }} name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
path: apps/desktop/upload path: apps/desktop/upload
nightly-server: nightly-server:
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy server nightly name: Deploy server nightly
strategy: strategy:
fail-fast: false fail-fast: false
@@ -109,7 +108,7 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Run the build - name: Run the build
uses: ./.github/actions/build-server uses: ./.github/actions/build-server
@@ -118,7 +117,7 @@ jobs:
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- name: Publish release - name: Publish release
uses: softprops/action-gh-release@v2.4.2 uses: softprops/action-gh-release@v2.3.2
if: ${{ github.event_name != 'pull_request' }} if: ${{ github.event_name != 'pull_request' }}
with: with:
make_latest: false make_latest: false

View File

@@ -4,8 +4,6 @@ on:
push: push:
branches: branches:
- main - main
paths-ignore:
- "apps/website/**"
pull_request: pull_request:
permissions: permissions:
@@ -16,26 +14,30 @@ jobs:
main: main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
filter: tree:0 filter: tree:0
fetch-depth: 0 fetch-depth: 0
# This enables task distribution via Nx Cloud
# Run this command as early as possible, before dependencies are installed
# Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun
# Connect your workspace by running "nx connect" and uncomment this line to enable task distribution
# - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e-ci"
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps - run: pnpm exec playwright install --with-deps
- uses: nrwl/nx-set-shas@v4
- run: pnpm --filter server-e2e e2e # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: npx nx-cloud record -- echo Hello World
- name: Upload test report # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
if: failure() # When you enable task distribution, run the e2e-ci task instead of e2e
uses: actions/upload-artifact@v5 - run: pnpm exec nx affected -t e2e --exclude desktop-e2e
with:
name: e2e report
path: apps/server-e2e/test-output

View File

@@ -30,30 +30,18 @@ jobs:
image: win-signing image: win-signing
shell: cmd shell: cmd
forge_platform: win32 forge_platform: win32
# Exclude ARM64 Linux from default matrix to use native runner
exclude:
- arch: arm64
os:
name: linux
# Add ARM64 Linux with native ubuntu-24.04-arm runner for better-sqlite3 compatibility
include:
- arch: arm64
os:
name: linux
image: ubuntu-24.04-arm
shell: bash
forge_platform: linux
runs-on: ${{ matrix.os.image }} runs-on: ${{ matrix.os.image }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
- name: Set up node & dependencies - name: Set up node & dependencies
uses: actions/setup-node@v6 uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 22
cache: 'pnpm' cache: 'pnpm'
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- uses: nrwl/nx-set-shas@v4
- name: Run the build - name: Run the build
uses: ./.github/actions/build-electron uses: ./.github/actions/build-electron
with: with:
@@ -70,10 +58,9 @@ jobs:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }} WINDOWS_SIGN_EXECUTABLE: ${{ vars.WINDOWS_SIGN_EXECUTABLE }}
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
- name: Upload the artifact - name: Upload the artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }} name: release-desktop-${{ matrix.os.name }}-${{ matrix.arch }}
path: apps/desktop/upload/*.* path: apps/desktop/upload/*.*
@@ -91,7 +78,7 @@ jobs:
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Run the build - name: Run the build
uses: ./.github/actions/build-server uses: ./.github/actions/build-server
@@ -100,7 +87,7 @@ jobs:
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- name: Upload the artifact - name: Upload the artifact
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v4
with: with:
name: release-server-linux-${{ matrix.arch }} name: release-server-linux-${{ matrix.arch }}
path: upload/*.* path: upload/*.*
@@ -114,20 +101,20 @@ jobs:
steps: steps:
- run: mkdir upload - run: mkdir upload
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
sparse-checkout: | sparse-checkout: |
docs/Release Notes docs/Release Notes
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v6 uses: actions/download-artifact@v4
with: with:
merge-multiple: true merge-multiple: true
pattern: release-* pattern: release-*
path: upload path: upload
- name: Publish stable release - name: Publish stable release
uses: softprops/action-gh-release@v2.4.2 uses: softprops/action-gh-release@v2.3.2
with: with:
draft: false draft: false
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md

View File

@@ -1,11 +0,0 @@
name: Unblock signing
on:
workflow_dispatch:
jobs:
unblock-win-signing:
runs-on: win-signing
steps:
- run: |
cat ${{ vars.WINDOWS_SIGN_ERROR_LOG }}
rm ${{ vars.WINDOWS_SIGN_ERROR_LOG }}

View File

@@ -1,51 +0,0 @@
name: Deploy website
on:
push:
branches:
- main
paths:
- "apps/website/**"
pull_request:
paths:
- "apps/website/**"
release:
types: [ released ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
name: Build & deploy website
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
cache: "pnpm"
- name: Install dependencies
run: pnpm install --filter website --frozen-lockfile
- name: Build the website
run: pnpm website:build
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
with:
project_name: "trilium-homepage"
comment_body: "📚 Website preview is ready"
production_url: "https://triliumnotes.org"
deploy_dir: "apps/website/dist"
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github_token: ${{ secrets.GITHUB_TOKEN }}

12
.gitignore vendored
View File

@@ -1,5 +1,4 @@
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
/.cache
# compiled output # compiled output
dist dist
@@ -11,7 +10,6 @@ node_modules
# IDEs and editors # IDEs and editors
/.idea /.idea
.idea
.project .project
.classpath .classpath
.c9/ .c9/
@@ -33,11 +31,14 @@ testem.log
.DS_Store .DS_Store
Thumbs.db Thumbs.db
.nx/cache
.nx/workspace-data
vite.config.*.timestamp* vite.config.*.timestamp*
vitest.config.*.timestamp* vitest.config.*.timestamp*
test-output test-output
apps/*/data* apps/*/data
apps/*/out apps/*/out
upload upload
@@ -45,7 +46,4 @@ upload
*.tsbuildinfo *.tsbuildinfo
/result /result
.svelte-kit .svelte-kit
# docs
site/

6
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,6 @@
# Default ignored files
/workspace.xml
# Datasource local storage ignored files
/dataSources.local.xml
/dataSources/

15
.idea/codeStyles/Project.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</value>
</option>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

12
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="document.db" uuid="2a4ac1e6-b828-4a2a-8e4a-3f59f10aff26">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/data/document.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

4
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>

15
.idea/git_toolbox_prj.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -0,0 +1,11 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

9
.idea/jsLinters/jslint.xml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JSLintConfiguration">
<option devel="true" />
<option es6="true" />
<option maxerr="50" />
<option node="true" />
</component>
</project>

8
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_16" default="true" project-jdk-name="openjdk-16" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/trilium.iml" filepath="$PROJECT_DIR$/trilium.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$" dialect="SQLite" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,2 +1,2 @@
zadam <adam.zivner@gmail.com> Adam Zivner <adam.zivner@gmail.com>
zadam <zadam.apps@gmail.com> Adam Zivner <zadam.apps@gmail.com>

2
.nvmrc
View File

@@ -1 +1 @@
24.11.0 22.17.0

View File

@@ -5,6 +5,7 @@
"lokalise.i18n-ally", "lokalise.i18n-ally",
"ms-azuretools.vscode-docker", "ms-azuretools.vscode-docker",
"ms-playwright.playwright", "ms-playwright.playwright",
"nrwl.angular-console",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"tobermory.es6-string-html", "tobermory.es6-string-html",
"vitest.explorer", "vitest.explorer",

View File

@@ -3,7 +3,6 @@
languageIds: languageIds:
- javascript - javascript
- typescript - typescript
- typescriptreact
- html - html
# An array of RegExes to find the key usage. **The key should be captured in the first match group**. # An array of RegExes to find the key usage. **The key should be captured in the first match group**.
@@ -14,7 +13,6 @@ usageMatchRegex:
# the `{key}` will be placed by a proper keypath matching regex, # the `{key}` will be placed by a proper keypath matching regex,
# you can ignore it and use your own matching rules as well # you can ignore it and use your own matching rules as well
- "[^\\w\\d]t\\(['\"`]({key})['\"`]" - "[^\\w\\d]t\\(['\"`]({key})['\"`]"
- <Trans\s*i18nKey="({key})"[^>]*>
# A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys # A RegEx to set a custom scope range. This scope will be used as a prefix when detecting keys
# and works like how the i18next framework identifies the namespace scope from the # and works like how the i18next framework identifies the namespace scope from the
@@ -27,10 +25,9 @@ scopeRangeRegex: "useTranslation\\(\\s*\\[?\\s*['\"`](.*?)['\"`]"
# The "$1" will be replaced by the keypath specified. # The "$1" will be replaced by the keypath specified.
refactorTemplates: refactorTemplates:
- t("$1") - t("$1")
- {t("$1")}
- ${t("$1")} - ${t("$1")}
- <%= t("$1") %> - <%= t("$1") %>
# If set to true, only enables this custom framework (will disable all built-in frameworks) # If set to true, only enables this custom framework (will disable all built-in frameworks)
monopoly: true monopoly: true

12
.vscode/settings.json vendored
View File

@@ -5,8 +5,7 @@
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [ "i18n-ally.localesPaths": [
"apps/server/src/assets/translations", "apps/server/src/assets/translations",
"apps/client/src/translations", "apps/client/src/translations"
"apps/website/public/translations"
], ],
"npm.exclude": [ "npm.exclude": [
"**/dist", "**/dist",
@@ -29,12 +28,5 @@
"typescript.validate.enable": true, "typescript.validate.enable": true,
"typescript.tsserver.experimental.enableProjectDiagnostics": true, "typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true
"search.exclude": {
"**/node_modules": true,
"docs/**/*.html": true,
"docs/**/*.png": true,
"apps/server/src/assets/doc_notes/**": true,
"apps/edit-docs/demo/**": true
}
} }

View File

@@ -20,10 +20,5 @@
"scope": "typescript", "scope": "typescript",
"prefix": "jqf", "prefix": "jqf",
"body": ["private $${1:name}!: JQuery<HTMLElement>;"] "body": ["private $${1:name}!: JQuery<HTMLElement>;"]
},
"region": {
"scope": "css",
"prefix": "region",
"body": ["/* #region ${1:name} */\n$0\n/* #endregion */"]
} }
} }

152
CLAUDE.md
View File

@@ -1,152 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
## Development Commands
### Setup
- `pnpm install` - Install all dependencies
- `corepack enable` - Enable pnpm if not available
### Running Applications
- `pnpm run server:start` - Start development server (http://localhost:8080)
- `pnpm run server:start-prod` - Run server in production mode
### Building
- `pnpm run client:build` - Build client application
- `pnpm run server:build` - Build server application
- `pnpm run electron:build` - Build desktop application
### Testing
- `pnpm test:all` - Run all tests (parallel + sequential)
- `pnpm test:parallel` - Run tests that can run in parallel
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
- `pnpm coverage` - Generate coverage reports
## Architecture Overview
### Monorepo Structure
- **apps/**: Runnable applications
- `client/` - Frontend application (shared by server and desktop)
- `server/` - Node.js server with web interface
- `desktop/` - Electron desktop application
- `web-clipper/` - Browser extension for saving web content
- Additional tools: `db-compare`, `dump-db`, `edit-docs`
- **packages/**: Shared libraries
- `commons/` - Shared interfaces and utilities
- `ckeditor5/` - Custom rich text editor with Trilium-specific plugins
- `codemirror/` - Code editor customizations
- `highlightjs/` - Syntax highlighting
- Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid`
### Core Architecture Patterns
#### Three-Layer Cache System
- **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`)
- **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`)
- **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`)
#### Entity System
Core entities are defined in `apps/server/src/becca/entities/`:
- `BNote` - Notes with content and metadata
- `BBranch` - Hierarchical relationships between notes (allows multiple parents)
- `BAttribute` - Key-value metadata attached to notes
- `BRevision` - Note version history
- `BOption` - Application configuration
#### Widget-Based UI
Frontend uses a widget system (`apps/client/src/widgets/`):
- `BasicWidget` - Base class for all UI components
- `NoteContextAwareWidget` - Widgets that respond to note changes
- `RightPanelWidget` - Widgets displayed in the right panel
- Type-specific widgets in `type_widgets/` directory
#### API Architecture
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
- **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`)
### Key Files for Understanding Architecture
1. **Application Entry Points**:
- `apps/server/src/main.ts` - Server startup
- `apps/client/src/desktop.ts` - Client initialization
2. **Core Services**:
- `apps/server/src/becca/becca.ts` - Backend data management
- `apps/client/src/services/froca.ts` - Frontend data synchronization
- `apps/server/src/services/backend_script_api.ts` - Scripting API
3. **Database Schema**:
- `apps/server/src/assets/db/schema.sql` - Core database structure
4. **Configuration**:
- `package.json` - Project dependencies and scripts
## Note Types and Features
Trilium supports multiple note types, each with specialized widgets:
- **Text**: Rich text with CKEditor5 (markdown import/export)
- **Code**: Syntax-highlighted code editing with CodeMirror
- **File**: Binary file attachments
- **Image**: Image display with editing capabilities
- **Canvas**: Drawing/diagramming with Excalidraw
- **Mermaid**: Diagram generation
- **Relation Map**: Visual note relationship mapping
- **Web View**: Embedded web pages
- **Doc/Book**: Hierarchical documentation structure
## Development Guidelines
### Testing Strategy
- Server tests run sequentially due to shared database
- Client tests can run in parallel
- E2E tests use Playwright for both server and desktop apps
- Build validation tests check artifact integrity
### Scripting System
Trilium provides powerful user scripting capabilities:
- Frontend scripts run in browser context
- Backend scripts run in Node.js context with full API access
- Script API documentation available in `docs/Script API/`
### Internationalization
- Translation files in `apps/client/src/translations/`
- Supported languages: English, German, Spanish, French, Romanian, Chinese
### Security Considerations
- Per-note encryption with granular protected sessions
- CSRF protection for API endpoints
- OpenID and TOTP authentication support
- Sanitization of user-generated content
## Common Development Tasks
### Adding New Note Types
1. Create widget in `apps/client/src/widgets/type_widgets/`
2. Register in `apps/client/src/services/note_types.ts`
3. Add backend handling in `apps/server/src/services/notes.ts`
### Extending Search
- Search expressions handled in `apps/server/src/services/search/`
- Add new search operators in search context files
### Custom CKEditor Plugins
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/`
- Update schema in `apps/server/src/assets/db/schema.sql`
## Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds

141
README.md
View File

@@ -1,22 +1,11 @@
<div align="center">
<sup>Special thanks to:</sup><br />
<a href="https://go.warp.dev/Trilium" target="_blank">
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png"><br />
Warp, built for coding with multiple AI agents<br />
</a>
<sup>Available for macOS, Linux and Windows</sup>
</div>
<hr />
# Trilium Notes # Trilium Notes
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran) ![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran) ![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran?style=flat-square)
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/trilium) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes?style=flat-square)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/trilium/total) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total?style=flat-square)
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [![Translation status](https://hosted.weblate.org/widget/trilium/svg-badge.svg)](https://hosted.weblate.org/engage/trilium/) [![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop&style=flat-square)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README-ru.md) | [Japanese](./docs/README-ja.md) | [Italian](./docs/README-it.md) | [Spanish](./docs/README-es.md) [English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
@@ -24,27 +13,6 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a> <a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
## ⏬ Download
- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) stable version, recommended for most users.
- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) unstable development version, updated daily with the latest features and fixes.
## 📚 Documentation
**Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)**
Our documentation is available in multiple formats:
- **Online Documentation**: Browse the full documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)
- **In-App Help**: Press `F1` within Trilium to access the same documentation directly in the application
- **GitHub**: Navigate through the [User Guide](./docs/User%20Guide/User%20Guide/) in this repository
### Quick Links
- [Getting Started Guide](https://docs.triliumnotes.org/)
- [Installation Instructions](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker Setup](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Basic Concepts and Features](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of Personal Knowledge Base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
## 🎁 Features ## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes)) * Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
@@ -78,15 +46,28 @@ Our documentation is available in multiple formats:
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more. - [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more. - [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
## Why TriliumNext? ## ⚠️ Why TriliumNext?
The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext [The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
### ⬆️Migrating from Zadam/Trilium? ### Migrating from Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database. There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration. Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
## 📖 Documentation
We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.
Below are some quick links for your convenience to navigate the documentation:
- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs).
## 💬 Discuss with us ## 💬 Discuss with us
@@ -94,14 +75,14 @@ Feel free to join our official conversations. We would love to hear what feature
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.) - [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join) - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.) - [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.) - [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
## 🏗 Installation ## 🏗 Installation
### Windows / MacOS ### Windows / MacOS
Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable. Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
### Linux ### Linux
@@ -109,7 +90,7 @@ If your distribution is listed in the table below, use your distribution's packa
[![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package and run the `trilium` executable. You may also download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub. TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
@@ -123,33 +104,23 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below). To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support. If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid.
### Server ### Server
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
## 💻 Contribute ## 💻 Contribute
### Translations
If you are a native speaker, help us translate Trilium by heading over to our [Weblate page](https://hosted.weblate.org/engage/trilium/).
Here's the language coverage we have so far:
[![Translation status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
### Code ### Code
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080): Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
```shell ```shell
git clone https://github.com/TriliumNext/Trilium.git git clone https://github.com/TriliumNext/Notes.git
cd Trilium cd Notes
pnpm install pnpm install
pnpm run server:start pnpm run server:start
``` ```
@@ -158,57 +129,39 @@ pnpm run server:start
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation: Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
```shell ```shell
git clone https://github.com/TriliumNext/Trilium.git git clone https://github.com/TriliumNext/Notes.git
cd Trilium cd Notes
pnpm install pnpm install
pnpm edit-docs:edit-docs pnpm nx run edit-docs:edit-docs
``` ```
### Building the Executable ### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows: Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell ```shell
git clone https://github.com/TriliumNext/Trilium.git git clone https://github.com/TriliumNext/Notes.git
cd Trilium cd Notes
pnpm install pnpm install
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32 pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
``` ```
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide). For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
### Developer Documentation ### Developer Documentation
Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above. Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
## 👏 Shoutouts ## 👏 Shoutouts
* [zadam](https://github.com/zadam) for the original concept and implementation of the application. * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the application icon. * [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
* [nriver](https://github.com/nriver) for his work on internationalization. * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas. * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
* [antoniotejada](https://github.com/nriver) for the original syntax highlight widget.
* [Dosu](https://dosu.dev/) for providing us with the automated responses to GitHub issues and discussions.
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
Trilium would not be possible without the technologies behind it:
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind text notes. We are grateful for being offered a set of the premium features.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map)
## 🤝 Support ## 🤝 Support
Trilium is built and maintained with [hundreds of hours of work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your support keeps it open-source, improves features, and covers costs such as hosting. Support for the TriliumNext organization will be possible in the near future. For now, you can:
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
Consider supporting the main developer ([eliandoran](https://github.com/eliandoran)) of the application via: - Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
- [PayPal](https://paypal.me/eliandoran)
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
## 🔑 License ## 🔑 License

View File

@@ -35,20 +35,22 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.56.1", "@playwright/test": "1.53.2",
"@stylistic/eslint-plugin": "5.5.0", "@stylistic/eslint-plugin": "5.1.0",
"@types/express": "5.0.5", "@types/express": "5.0.3",
"@types/node": "24.10.0", "@types/node": "22.16.2",
"@types/yargs": "17.0.34", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"eslint": "9.39.1", "eslint": "9.30.1",
"eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25", "esm": "3.2.25",
"jsdoc": "4.0.5", "jsdoc": "4.0.4",
"lorem-ipsum": "2.0.8", "lorem-ipsum": "2.0.8",
"rcedit": "5.0.0", "rcedit": "4.0.1",
"rimraf": "6.1.0", "rimraf": "6.0.1",
"tslib": "2.8.1" "tslib": "2.8.1",
"typedoc": "0.28.7",
"typedoc-plugin-missing-exports": "4.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"appdmg": "0.6.6" "appdmg": "0.6.6"

View File

@@ -1,3 +1,4 @@
import type child_process from "child_process";
import { describe, beforeAll, afterAll } from "vitest"; import { describe, beforeAll, afterAll } from "vitest";
let etapiAuthToken: string | undefined; let etapiAuthToken: string | undefined;
@@ -11,6 +12,8 @@ type SpecDefinitionsFunc = () => void;
function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void { function describeEtapi(description: string, specDefinitions: SpecDefinitionsFunc): void {
describe(description, () => { describe(description, () => {
let appProcess: ReturnType<typeof child_process.spawn>;
beforeAll(async () => {}); beforeAll(async () => {});
afterAll(() => {}); afterAll(() => {});

15
_regroup/typedoc.json Normal file
View File

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

View File

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

View File

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

View File

@@ -1,147 +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 importAndExportDocs(sourcePath: string, outputSubDir: string) {
const note = await importData(sourcePath);
// Use a meaningful name for the temporary zip file
const zipName = outputSubDir || "user-guide";
const zipFilePath = `output-${zipName}.zip`;
try {
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
const branch = note.getParentBranches()[0];
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
"no-progress-reporting",
"export",
null
);
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
await exportToZip(taskContext, branch, "share", fileOutputStream);
await waitForStreamToFinish(fileOutputStream);
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
const outputPath = outputSubDir ? join(OUTPUT_DIR, outputSubDir) : OUTPUT_DIR;
await extractZip(zipFilePath, outputPath);
} finally {
if (await fsExtra.exists(zipFilePath)) {
await fsExtra.rm(zipFilePath);
}
}
}
async function buildDocsInner() {
const i18n = await import("@triliumnext/server/src/services/i18n.js");
await i18n.initializeTranslations();
const sqlInit = (await import("../../server/src/services/sql_init.js")).default;
await sqlInit.createInitialDatabase(true);
// Wait for becca to be loaded before importing data
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
// Build User Guide
console.log("Building User Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
// Build Developer Guide
console.log("Building Developer Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
// Copy favicon.
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
console.log("Documentation built successfully!");
}
export async function importData(path: string) {
const buffer = await createImportZip(path);
const importService = (await import("../../server/src/services/import/zip.js")).default;
const TaskContext = (await import("../../server/src/services/task_context.js")).default;
const context = new TaskContext("no-progress-reporting", "importNotes", null);
const becca = (await import("../../server/src/becca/becca.js")).default;
const rootNote = becca.getRoot();
if (!rootNote) {
throw new Error("Missing root note for import.");
}
return await importService.importZip(context, buffer, rootNote, {
preserveIds: true
});
}
async function createImportZip(path: string) {
const inputFile = "input.zip";
const archive = archiver("zip", {
zlib: { level: 0 }
});
console.log("Archive path is ", resolve(path))
archive.directory(path, "/");
const outputStream = fsExtra.createWriteStream(inputFile);
archive.pipe(outputStream);
archive.finalize();
await waitForStreamToFinish(outputStream);
try {
return await fsExtra.readFile(inputFile);
} finally {
await fsExtra.rm(inputFile);
}
}
function waitForStreamToFinish(stream: WriteStream) {
return new Promise<void>((res, rej) => {
stream.on("finish", () => res());
stream.on("error", (err) => rej(err));
});
}
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
// We ignore directories since they can appear out of order anyway.
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
const destPath = join(outputPath, entry.fileName);
const fileContent = await readContent(zip, entry);
await fsExtra.mkdirs(dirname(destPath));
await fs.writeFile(destPath, fileContent);
}
zip.readEntry();
});
}
export default async function buildDocs({ gitRootDir }: BuildContext) {
// Build the share theme.
execSync(`pnpm run --filter share-theme build`, {
stdio: "inherit",
cwd: gitRootDir
});
// Trigger the actual build.
await new Promise((res, rej) => {
cls.init(() => {
buildDocsInner()
.catch(rej)
.then(res);
});
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
# The development license key for premium CKEditor features. # The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project. # Note: This key must only be used for the Trilium Notes project.
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw # Expires on: 2025-09-13
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w
VITE_CKEDITOR_ENABLE_INSPECTOR=false VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

@@ -1,30 +1,24 @@
{ {
"name": "@triliumnext/client", "name": "@triliumnext/client",
"version": "0.99.4", "version": "0.96.0",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true, "private": true,
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"author": { "author": {
"name": "Trilium Notes Team", "name": "Trilium Notes Team",
"email": "contact@eliandoran.me", "email": "contact@eliandoran.me",
"url": "https://github.com/TriliumNext/Trilium" "url": "https://github.com/TriliumNext/Notes"
},
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"test": "vitest",
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
}, },
"dependencies": { "dependencies": {
"@eslint/js": "9.39.1", "@eslint/js": "9.30.1",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.19", "@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.19", "@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.19", "@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.19", "@fullcalendar/list": "6.1.18",
"@fullcalendar/multimonth": "6.1.19", "@fullcalendar/multimonth": "6.1.18",
"@fullcalendar/timegrid": "6.1.19", "@fullcalendar/timegrid": "6.1.18",
"@maplibre/maplibre-gl-leaflet": "0.1.3", "@mermaid-js/layout-elk": "0.1.8",
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.0", "@mind-elixir/node-menu": "5.0.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*", "@triliumnext/ckeditor5": "workspace:*",
@@ -32,52 +26,61 @@
"@triliumnext/commons": "workspace:*", "@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*", "@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*", "@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
"bootstrap": "5.3.8", "bootstrap": "5.3.7",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"color": "5.0.2", "dayjs": "1.11.13",
"dayjs": "1.11.19",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "3.0.0", "debounce": "2.2.0",
"draggabilly": "3.0.0", "draggabilly": "3.0.0",
"force-graph": "1.51.0", "force-graph": "1.50.1",
"globals": "16.5.0", "globals": "16.3.0",
"i18next": "25.6.1", "i18next": "25.3.2",
"i18next-http-backend": "3.0.2", "i18next-http-backend": "3.0.2",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.5", "jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6", "jsplumb": "2.15.6",
"katex": "0.16.25", "katex": "0.16.22",
"knockout": "3.5.1", "knockout": "3.5.1",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-gpx": "2.2.0", "leaflet-gpx": "2.2.0",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "16.4.2", "marked": "16.0.0",
"mermaid": "11.12.1", "mermaid": "11.8.1",
"mind-elixir": "5.3.5", "mind-elixir": "5.0.1",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"preact": "10.27.2", "preact": "10.26.9",
"react-i18next": "16.2.4", "split.js": "1.6.5",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1", "tabulator-tables": "6.3.1",
"vanilla-js-wheel-zoom": "9.0.4" "vanilla-js-wheel-zoom": "9.0.4"
}, },
"devDependencies": { "devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0", "@ckeditor/ckeditor5-inspector": "4.1.0",
"@preact/preset-vite": "2.10.2",
"@types/bootstrap": "5.2.10", "@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33", "@types/jquery": "3.5.32",
"@types/leaflet": "1.9.21", "@types/leaflet": "1.9.20",
"@types/leaflet-gpx": "1.3.8", "@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12", "@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.1", "@types/tabulator-tables": "6.2.7",
"@types/tabulator-tables": "6.3.0", "copy-webpack-plugin": "13.0.0",
"copy-webpack-plugin": "13.0.1", "happy-dom": "18.0.1",
"happy-dom": "20.0.10",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4" "vite-plugin-static-copy": "3.1.0"
},
"nx": {
"name": "client",
"targets": {
"serve": {
"dependsOn": [
"^build"
]
},
"circular-deps": {
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
}
}
} }
} }

View File

@@ -7,9 +7,6 @@
"display": "standalone", "display": "standalone",
"scope": "/", "scope": "/",
"start_url": "/", "start_url": "/",
"display_override": [
"window-controls-overlay"
],
"icons": [ "icons": [
{ {
"src": "icon.png", "src": "icon.png",

View File

@@ -1,6 +1,6 @@
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import RootCommandExecutor from "./root_command_executor.js"; import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints from "./entrypoints.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
import options from "../services/options.js"; import options from "../services/options.js";
import utils, { hasTouchBar } from "../services/utils.js"; import utils, { hasTouchBar } from "../services/utils.js";
import zoomComponent from "./zoom.js"; import zoomComponent from "./zoom.js";
@@ -28,17 +28,16 @@ import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror"; import type CodeMirror from "@triliumnext/codemirror";
import { StartupChecks } from "./startup_checks.js"; import { StartupChecks } from "./startup_checks.js";
import type { CreateNoteOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
import type RootContainer from "../widgets/containers/root_container.js";
import { SqlExecuteResults } from "@triliumnext/commons";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootContainer; getRootWidget: (appContext: AppContext) => RootWidget;
} }
export interface BeforeUploadListener extends Component { interface RootWidget extends Component {
render: () => JQuery<HTMLElement>;
}
interface BeforeUploadListener extends Component {
beforeUnloadEvent(): boolean; beforeUnloadEvent(): boolean;
} }
@@ -83,6 +82,7 @@ export type CommandMappings = {
focusTree: CommandData; focusTree: CommandData;
focusOnTitle: CommandData; focusOnTitle: CommandData;
focusOnDetail: CommandData; focusOnDetail: CommandData;
focusOnSearchDefinition: Required<CommandData>;
searchNotes: CommandData & { searchNotes: CommandData & {
searchString?: string; searchString?: string;
ancestorNoteId?: string | null; ancestorNoteId?: string | null;
@@ -90,14 +90,7 @@ export type CommandMappings = {
closeTocCommand: CommandData; closeTocCommand: CommandData;
closeHlt: CommandData; closeHlt: CommandData;
showLaunchBarSubtree: CommandData; showLaunchBarSubtree: CommandData;
showHiddenSubtree: CommandData; showRevisions: CommandData;
showSQLConsoleHistory: CommandData;
logout: CommandData;
switchToMobileVersion: CommandData;
switchToDesktopVersion: CommandData;
showRevisions: CommandData & {
noteId?: string | null;
};
showLlmChat: CommandData; showLlmChat: CommandData;
createAiChat: CommandData; createAiChat: CommandData;
showOptions: CommandData & { showOptions: CommandData & {
@@ -116,7 +109,7 @@ export type CommandMappings = {
openedFileUpdated: CommandData & { openedFileUpdated: CommandData & {
entityType: string; entityType: string;
entityId: string; entityId: string;
lastModifiedMs?: number; lastModifiedMs: number;
filePath: string; filePath: string;
}; };
focusAndSelectTitle: CommandData & { focusAndSelectTitle: CommandData & {
@@ -138,9 +131,6 @@ export type CommandMappings = {
hideLeftPane: CommandData; hideLeftPane: CommandData;
showCpuArchWarning: CommandData; showCpuArchWarning: CommandData;
showLeftPane: CommandData; showLeftPane: CommandData;
showAttachments: CommandData;
showSearchHistory: CommandData;
showShareSubtree: CommandData;
hoistNote: CommandData & { noteId: string }; hoistNote: CommandData & { noteId: string };
leaveProtectedSession: CommandData; leaveProtectedSession: CommandData;
enterProtectedSession: CommandData; enterProtectedSession: CommandData;
@@ -181,7 +171,7 @@ export type CommandMappings = {
deleteNotes: ContextMenuCommandData; deleteNotes: ContextMenuCommandData;
importIntoNote: ContextMenuCommandData; importIntoNote: ContextMenuCommandData;
exportNote: ContextMenuCommandData; exportNote: ContextMenuCommandData;
searchInSubtree: CommandData & { notePath: string; }; searchInSubtree: ContextMenuCommandData;
moveNoteUp: ContextMenuCommandData; moveNoteUp: ContextMenuCommandData;
moveNoteDown: ContextMenuCommandData; moveNoteDown: ContextMenuCommandData;
moveNoteUpInHierarchy: ContextMenuCommandData; moveNoteUpInHierarchy: ContextMenuCommandData;
@@ -218,12 +208,12 @@ export type CommandMappings = {
/** Works only in the electron context menu. */ /** Works only in the electron context menu. */
replaceMisspelling: CommandData; replaceMisspelling: CommandData;
importMarkdownInline: CommandData;
showPasswordNotSet: CommandData; showPasswordNotSet: CommandData;
showProtectedSessionPasswordDialog: CommandData; showProtectedSessionPasswordDialog: CommandData;
showUploadAttachmentsDialog: CommandData & { noteId: string }; showUploadAttachmentsDialog: CommandData & { noteId: string };
showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget }; showIncludeNoteDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string }; showAddLinkDialog: CommandData & { textTypeWidget: EditableTextTypeWidget, text: string };
showPasteMarkdownDialog: CommandData & { textTypeWidget: EditableTextTypeWidget };
closeProtectedSessionPasswordDialog: CommandData; closeProtectedSessionPasswordDialog: CommandData;
copyImageReferenceToClipboard: CommandData; copyImageReferenceToClipboard: CommandData;
copyImageToClipboard: CommandData; copyImageToClipboard: CommandData;
@@ -270,75 +260,6 @@ export type CommandMappings = {
closeThisNoteSplit: CommandData; closeThisNoteSplit: CommandData;
moveThisNoteSplit: CommandData & { isMovingLeft: boolean }; moveThisNoteSplit: CommandData & { isMovingLeft: boolean };
jumpToNote: CommandData; jumpToNote: CommandData;
openTodayNote: CommandData;
commandPalette: CommandData;
// Keyboard shortcuts
backInNoteHistory: CommandData;
forwardInNoteHistory: CommandData;
forceSaveRevision: CommandData;
scrollToActiveNote: CommandData;
quickSearch: CommandData;
collapseTree: CommandData;
createNoteAfter: CommandData;
createNoteInto: CommandData;
addNoteAboveToSelection: CommandData;
addNoteBelowToSelection: CommandData;
openNewTab: CommandData;
activateNextTab: CommandData;
activatePreviousTab: CommandData;
openNewWindow: CommandData;
toggleTray: CommandData;
firstTab: CommandData;
secondTab: CommandData;
thirdTab: CommandData;
fourthTab: CommandData;
fifthTab: CommandData;
sixthTab: CommandData;
seventhTab: CommandData;
eigthTab: CommandData;
ninthTab: CommandData;
lastTab: CommandData;
showNoteSource: CommandData;
showSQLConsole: CommandData;
showBackendLog: CommandData;
showCheatsheet: CommandData;
showHelp: CommandData;
addLinkToText: CommandData;
followLinkUnderCursor: CommandData;
insertDateTimeToText: CommandData;
pasteMarkdownIntoText: CommandData;
cutIntoNote: CommandData;
addIncludeNoteToText: CommandData;
editReadOnlyNote: CommandData;
toggleRibbonTabClassicEditor: CommandData;
toggleRibbonTabBasicProperties: CommandData;
toggleRibbonTabBookProperties: CommandData;
toggleRibbonTabFileProperties: CommandData;
toggleRibbonTabImageProperties: CommandData;
toggleRibbonTabOwnedAttributes: CommandData;
toggleRibbonTabInheritedAttributes: CommandData;
toggleRibbonTabPromotedAttributes: CommandData;
toggleRibbonTabNoteMap: CommandData;
toggleRibbonTabNoteInfo: CommandData;
toggleRibbonTabNotePaths: CommandData;
toggleRibbonTabSimilarNotes: CommandData;
toggleRightPane: CommandData;
printActiveNote: CommandData;
exportAsPdf: CommandData;
openNoteExternally: CommandData;
openNoteCustom: CommandData;
renderActiveNote: CommandData;
unhoist: CommandData;
reloadFrontendApp: CommandData;
openDevTools: CommandData;
findInText: CommandData;
toggleLeftPane: CommandData;
toggleFullscreen: CommandData;
zoomOut: CommandData;
zoomIn: CommandData;
zoomReset: CommandData;
copyWithoutFormatting: CommandData;
// Geomap // Geomap
deleteFromMap: { noteId: string }; deleteFromMap: { noteId: string };
@@ -355,30 +276,12 @@ export type CommandMappings = {
geoMapCreateChildNote: CommandData; geoMapCreateChildNote: CommandData;
// Table view
addNewRow: CommandData & {
customOpts: CreateNoteOpts;
parentNotePath?: string;
};
addNewTableColumn: CommandData & {
columnToEdit?: ColumnComponent;
referenceColumn?: ColumnComponent;
direction?: "before" | "after";
type?: "label" | "relation";
};
deleteTableColumn: CommandData & {
columnToDelete?: ColumnComponent;
};
buildTouchBar: CommandData & { buildTouchBar: CommandData & {
TouchBar: typeof TouchBar; TouchBar: typeof TouchBar;
buildIcon(name: string): NativeImage; buildIcon(name: string): NativeImage;
}; };
refreshTouchBar: CommandData; refreshTouchBar: CommandData;
reloadTextEditor: CommandData; reloadTextEditor: CommandData;
chooseNoteType: CommandData & {
callback: ChooseNoteTypeCallback
}
}; };
type EventMappings = { type EventMappings = {
@@ -499,10 +402,6 @@ type EventMappings = {
noteIds: string[]; noteIds: string[];
}; };
refreshData: { ntxId: string | null | undefined }; refreshData: { ntxId: string | null | undefined };
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
}
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {
@@ -535,7 +434,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
export class AppContext extends Component { export class AppContext extends Component {
isMainWindow: boolean; isMainWindow: boolean;
components: Component[]; components: Component[];
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[]; beforeUnloadListeners: WeakRef<BeforeUploadListener>[];
tabManager!: TabManager; tabManager!: TabManager;
layout?: Layout; layout?: Layout;
noteTreeWidget?: NoteTreeWidget; noteTreeWidget?: NoteTreeWidget;
@@ -628,7 +527,7 @@ export class AppContext extends Component {
component.triggerCommand(commandName, { $el: $(this) }); component.triggerCommand(commandName, { $el: $(this) });
}); });
this.child(rootWidget as Component); this.child(rootWidget);
this.triggerEvent("initialRenderComplete", {}); this.triggerEvent("initialRenderComplete", {});
} }
@@ -655,20 +554,16 @@ export class AppContext extends Component {
} }
getComponentByEl(el: HTMLElement) { getComponentByEl(el: HTMLElement) {
return $(el).closest("[data-component-id]").prop("component"); return $(el).closest(".component").prop("component");
} }
addBeforeUnloadListener(obj: BeforeUploadListener | (() => boolean)) { addBeforeUnloadListener(obj: BeforeUploadListener) {
if (typeof WeakRef !== "function") { if (typeof WeakRef !== "function") {
// older browsers don't support WeakRef // older browsers don't support WeakRef
return; return;
} }
if (typeof obj === "object") { this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
this.beforeUnloadListeners.push(new WeakRef<BeforeUploadListener>(obj));
} else {
this.beforeUnloadListeners.push(obj);
}
} }
} }
@@ -678,29 +573,25 @@ const appContext = new AppContext(window.glob.isMainWindow);
$(window).on("beforeunload", () => { $(window).on("beforeunload", () => {
let allSaved = true; let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => typeof wr === "function" || !!wr.deref()); appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter((wr) => !!wr.deref());
for (const listener of appContext.beforeUnloadListeners) { for (const weakRef of appContext.beforeUnloadListeners) {
if (typeof listener === "object") { const component = weakRef.deref();
const component = listener.deref();
if (!component) { if (!component) {
continue; continue;
} }
if (!component.beforeUnloadEvent()) { if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`); console.log(`Component ${component.componentId} is not finished saving its state.`);
allSaved = false;
} toast.showMessage(t("app_context.please_wait_for_save"), 10000);
} else {
if (!listener()) { allSaved = false;
allSaved = false;
}
} }
} }
if (!allSaved) { if (!allSaved) {
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
return "some string"; return "some string";
} }
}); });

View File

@@ -1,8 +1,6 @@
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js"; import type { CommandMappings, CommandNames, EventData, EventNames } from "./app_context.js";
type EventHandler = ((data: any) => void);
/** /**
* Abstract class for all components in the Trilium's frontend. * Abstract class for all components in the Trilium's frontend.
* *
@@ -21,7 +19,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
initialized: Promise<void> | null; initialized: Promise<void> | null;
parent?: TypedComponent<any>; parent?: TypedComponent<any>;
_position!: number; _position!: number;
private listeners: Record<string, EventHandler[]> | null = {};
constructor() { constructor() {
this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`; this.componentId = `${this.sanitizedClassName}-${utils.randomString(8)}`;
@@ -79,14 +76,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null { handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
const promises: Promise<unknown>[] = []; const promises: Promise<unknown>[] = [];
// Handle React children.
if (this.listeners?.[name]) {
for (const listener of this.listeners[name]) {
listener(data);
}
}
// Handle legacy children.
for (const child of this.children) { for (const child of this.children) {
const ret = child.handleEvent(name, data) as Promise<void>; const ret = child.handleEvent(name, data) as Promise<void>;
@@ -131,35 +120,6 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return promise; return promise;
} }
registerHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners) {
this.listeners = {};
}
if (!this.listeners[name]) {
this.listeners[name] = [];
}
if (this.listeners[name].includes(handler)) {
return;
}
this.listeners[name].push(handler);
}
removeHandler<T extends EventNames>(name: T, handler: EventHandler) {
if (!this.listeners?.[name]?.includes(handler)) {
return;
}
this.listeners[name] = this.listeners[name]
.filter(listener => listener !== handler);
if (!this.listeners[name].length) {
delete this.listeners[name];
}
}
} }
export default class Component extends TypedComponent<Component> {} export default class Component extends TypedComponent<Component> {}

View File

@@ -10,16 +10,38 @@ import bundleService from "../services/bundle.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import linkService from "../services/link.js"; import linkService from "../services/link.js";
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons"; import type FNote from "../entities/fnote.js";
// TODO: Move somewhere else nicer.
export type SqlExecuteResults = string[][][];
// TODO: Deduplicate with server.
interface SqlExecuteResponse {
success: boolean;
error?: string;
results: SqlExecuteResults;
}
// TODO: Deduplicate with server.
interface CreateChildrenResponse {
note: FNote;
}
export default class Entrypoints extends Component { export default class Entrypoints extends Component {
constructor() { constructor() {
super(); super();
if (jQuery.hotkeys) {
// hot keys are active also inside inputs and content editables
jQuery.hotkeys.options.filterInputAcceptingElements = false;
jQuery.hotkeys.options.filterContentEditable = false;
jQuery.hotkeys.options.filterTextInputs = false;
}
} }
openDevToolsCommand() { openDevToolsCommand() {
if (utils.isElectron()) { if (utils.isElectron()) {
utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools(); utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
} }
} }
@@ -91,9 +113,7 @@ export default class Entrypoints extends Component {
if (win.isFullScreenable()) { if (win.isFullScreenable()) {
win.setFullScreen(!win.isFullScreen()); win.setFullScreen(!win.isFullScreen());
} }
} else { } // outside of electron this is handled by the browser
document.documentElement.requestFullscreen();
}
} }
reloadFrontendAppCommand() { reloadFrontendAppCommand() {
@@ -109,7 +129,7 @@ export default class Entrypoints extends Component {
if (utils.isElectron()) { if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron // standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
const activeIndex = webContents.navigationHistory.getActiveIndex(); const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex - 1); webContents.goToIndex(activeIndex - 1);
} else { } else {
@@ -121,7 +141,7 @@ export default class Entrypoints extends Component {
if (utils.isElectron()) { if (utils.isElectron()) {
// standard JS version does not work completely correctly in electron // standard JS version does not work completely correctly in electron
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
const activeIndex = webContents.navigationHistory.getActiveIndex(); const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
webContents.goToIndex(activeIndex + 1); webContents.goToIndex(activeIndex + 1);
} else { } else {
@@ -159,16 +179,6 @@ export default class Entrypoints extends Component {
this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" }); this.openInWindowCommand({ notePath: "", hoistedNoteId: "root" });
} }
async openTodayNoteCommand() {
const todayNote = await dateNoteService.getTodayNote();
if (!todayNote) {
console.warn("Missing today note.");
return;
}
await appContext.tabManager.openInSameTab(todayNote.noteId);
}
async runActiveNoteCommand() { async runActiveNoteCommand() {
const noteContext = appContext.tabManager.getActiveContext(); const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) { if (!noteContext) {

View File

@@ -325,12 +325,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return false; return false;
} }
// Collections must always display a note list, even if no children. // Some book types must always display a note list, even if no children.
if (note.type === "book") { if (["calendar", "table", "geoMap"].includes(note.getLabelValue("viewType") ?? "")) {
const viewType = note.getLabelValue("viewType") ?? "grid"; return true;
if (!["list", "grid"].includes(viewType)) {
return true;
}
} }
if (!note.hasChildren()) { if (!note.hasChildren()) {
@@ -440,22 +437,4 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
} }
} }
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
const ntxId = $(evt?.target as Element)
.closest("[data-ntx-id]")
.attr("data-ntx-id");
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
if (noteContext) {
noteContext.setNote(notePath, { viewScope }).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
}
});
} else {
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
}
}
export default NoteContext; export default NoteContext;

View File

@@ -7,6 +7,7 @@ import protectedSessionService from "../services/protected_session.js";
import options from "../services/options.js"; import options from "../services/options.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import LlmChatPanel from "../widgets/llm_chat_panel.js";
import toastService from "../services/toast.js"; import toastService from "../services/toast.js";
import noteCreateService from "../services/note_create.js"; import noteCreateService from "../services/note_create.js";
@@ -42,6 +43,8 @@ export default class RootCommandExecutor extends Component {
const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, { const noteContext = await appContext.tabManager.openTabWithNoteWithHoisting(searchNote.noteId, {
activate: true activate: true
}); });
appContext.triggerCommand("focusOnSearchDefinition", { ntxId: noteContext.ntxId });
} }
async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) { async searchInSubtreeCommand({ notePath }: CommandListenerData<"searchInSubtree">) {
@@ -170,8 +173,7 @@ export default class RootCommandExecutor extends Component {
} }
toggleTrayCommand() { toggleTrayCommand() {
if (!utils.isElectron() || options.is("disableTray")) return; if (!utils.isElectron()) return;
const { BrowserWindow } = utils.dynamicRequire("@electron/remote"); const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[]; const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
const isVisible = windows.every((w) => w.isVisible()); const isVisible = windows.every((w) => w.isVisible());

View File

@@ -433,9 +433,6 @@ export default class TabManager extends Component {
$autocompleteEl.autocomplete("close"); $autocompleteEl.autocomplete("close");
} }
// close dangling tooltips
$("body > div.tooltip").remove();
const noteContextsToRemove = noteContextToRemove.getSubContexts(); const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
@@ -603,18 +600,18 @@ export default class TabManager extends Component {
} }
async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) { async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId); const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
const removed = await this.removeNoteContext(ntxId); const removed = await this.removeNoteContext(ntxId);
if (removed) { if (removed) {
this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
} }
} }
async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) { async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
const { notePath, hoistedNoteId, viewScope } = this.getNoteContextById(ntxId); const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
this.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
} }
async reopenLastTabCommand() { async reopenLastTabCommand() {

View File

@@ -23,11 +23,11 @@ export default class TouchBarComponent extends Component {
this.$widget = $("<div>"); this.$widget = $("<div>");
$(window).on("focusin", async (e) => { $(window).on("focusin", async (e) => {
const focusedEl = e.target as unknown as HTMLElement; const $target = $(e.target);
const $target = $(focusedEl);
this.$activeModal = $target.closest(".modal-dialog"); this.$activeModal = $target.closest(".modal-dialog");
this.lastFocusedComponent = appContext.getComponentByEl(focusedEl); const parentComponentEl = $target.closest(".component");
this.lastFocusedComponent = appContext.getComponentByEl(parentComponentEl[0]);
this.#refreshTouchBar(); this.#refreshTouchBar();
}); });
} }

View File

@@ -8,9 +8,12 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import { t } from "./services/i18n.js"; import { t } from "./services/i18n.js";
import options from "./services/options.js"; import options from "./services/options.js";
import server from "./services/server.js";
import type ElectronRemote from "@electron/remote"; import type ElectronRemote from "@electron/remote";
import type Electron from "electron"; import type Electron from "electron";
import "./stylesheets/bootstrap.scss";
import "boxicons/css/boxicons.min.css"; import "boxicons/css/boxicons.min.css";
import "jquery-hotkeys";
import "autocomplete.js/index_jquery.js"; import "autocomplete.js/index_jquery.js";
await appContext.earlyInit(); await appContext.earlyInit();
@@ -44,10 +47,6 @@ if (utils.isElectron()) {
electronContextMenu.setupContextMenu(); electronContextMenu.setupContextMenu();
} }
if (utils.isPWA()) {
initPWATopbarColor();
}
function initOnElectron() { function initOnElectron() {
const electron: typeof Electron = utils.dynamicRequire("electron"); const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName)); electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
@@ -116,20 +115,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote; const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
nativeTheme.themeSource = themeSource; nativeTheme.themeSource = themeSource;
} }
function initPWATopbarColor() {
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}

View File

@@ -1,5 +1,6 @@
import server from "../services/server.js"; import server from "../services/server.js";
import noteAttributeCache from "../services/note_attribute_cache.js"; import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import cssClassManager from "../services/css_class_manager.js"; import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js"; import type { Froca } from "../services/froca-interface.js";
@@ -63,7 +64,7 @@ export interface NoteMetaData {
/** /**
* Note is the main node and concept in Trilium. * Note is the main node and concept in Trilium.
*/ */
export default class FNote { class FNote {
private froca: Froca; private froca: Froca;
noteId!: string; noteId!: string;
@@ -255,22 +256,6 @@ export default class FNote {
return this.children; return this.children;
} }
async getSubtreeNoteIds(includeArchived = false) {
let noteIds: (string | string[])[] = [];
for (const child of await this.getChildNotes()) {
if (child.isArchived && !includeArchived) continue;
noteIds.push(child.noteId);
noteIds.push(await child.getSubtreeNoteIds(includeArchived));
}
return noteIds.flat();
}
async getSubtreeNotes() {
const noteIds = await this.getSubtreeNoteIds();
return (await this.froca.getNotes(noteIds));
}
async getChildNotes() { async getChildNotes() {
return await this.froca.getNotes(this.children); return await this.froca.getNotes(this.children);
} }
@@ -417,7 +402,7 @@ export default class FNote {
return notePaths; return notePaths;
} }
getSortedNotePathRecords(hoistedNoteId = "root", activeNotePath: string | null = null): NotePathRecord[] { getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
const isHoistedRoot = hoistedNoteId === "root"; const isHoistedRoot = hoistedNoteId === "root";
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
@@ -428,23 +413,7 @@ export default class FNote {
isHidden: path.includes("_hidden") isHidden: path.includes("_hidden")
})); }));
// Calculate the length of the prefix match between two arrays
const prefixMatchLength = (path: string[], target: string[]) => {
const diffIndex = path.findIndex((seg, i) => seg !== target[i]);
return diffIndex === -1 ? Math.min(path.length, target.length) : diffIndex;
};
notePaths.sort((a, b) => { notePaths.sort((a, b) => {
if (activeNotePath) {
const activeSegments = activeNotePath.split('/');
const aOverlap = prefixMatchLength(a.notePath, activeSegments);
const bOverlap = prefixMatchLength(b.notePath, activeSegments);
// Paths with more matching prefix segments are prioritized
// when the match count is equal, other criteria are used for sorting
if (bOverlap !== aOverlap) {
return bOverlap - aOverlap;
}
}
if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { if (a.isInHoistedSubTree !== b.isInHoistedSubTree) {
return a.isInHoistedSubTree ? -1 : 1; return a.isInHoistedSubTree ? -1 : 1;
} else if (a.isArchived !== b.isArchived) { } else if (a.isArchived !== b.isArchived) {
@@ -465,11 +434,10 @@ export default class FNote {
* Returns the note path considered to be the "best" * Returns the note path considered to be the "best"
* *
* @param {string} [hoistedNoteId='root'] * @param {string} [hoistedNoteId='root']
* @param {string|null} [activeNotePath=null]
* @return {string[]} array of noteIds constituting the particular note path * @return {string[]} array of noteIds constituting the particular note path
*/ */
getBestNotePath(hoistedNoteId = "root", activeNotePath: string | null = null) { getBestNotePath(hoistedNoteId = "root") {
return this.getSortedNotePathRecords(hoistedNoteId, activeNotePath)[0]?.notePath; return this.getSortedNotePathRecords(hoistedNoteId)[0]?.notePath;
} }
/** /**
@@ -602,7 +570,7 @@ export default class FNote {
let childBranches = this.getChildBranches(); let childBranches = this.getChildBranches();
if (!childBranches) { if (!childBranches) {
console.error(`No children for '${this.noteId}'. This shouldn't happen.`); ws.logError(`No children for '${this.noteId}'. This shouldn't happen.`);
return []; return [];
} }
@@ -923,8 +891,8 @@ export default class FNote {
return this.getBlob(); return this.getBlob();
} }
getBlob() { async getBlob() {
return this.froca.getBlob("notes", this.noteId); return await this.froca.getBlob("notes", this.noteId);
} }
toString() { toString() {
@@ -1038,14 +1006,6 @@ export default class FNote {
return this.noteId.startsWith("_options"); return this.noteId.startsWith("_options");
} }
isTriliumSqlite() {
return this.mime === "text/x-sqlite;schema=trilium";
}
isTriliumScript() {
return this.mime.startsWith("application/javascript");
}
/** /**
* Provides note's date metadata. * Provides note's date metadata.
*/ */
@@ -1053,3 +1013,5 @@ export default class FNote {
return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`); return await server.get<NoteMetaData>(`notes/${this.noteId}/metadata`);
} }
} }
export default FNote;

View File

@@ -1,49 +1,78 @@
import { applyModals } from "./layout_commons.js";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import ContentHeader from "../widgets/containers/content-header.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import FindWidget from "../widgets/find.js";
import FlexContainer from "../widgets/containers/flex_container.js"; import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx"; import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import options from "../services/options.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
import TabRowWidget from "../widgets/tab_row.js"; import TabRowWidget from "../widgets/tab_row.js";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx"; import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteTitleWidget from "../widgets/note_title.js";
import OwnedAttributeListWidget from "../widgets/ribbon_widgets/owned_attribute_list.js";
import NoteActionsWidget from "../widgets/buttons/note_actions.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import RibbonContainer from "../widgets/containers/ribbon_container.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import InheritedAttributesWidget from "../widgets/ribbon_widgets/inherited_attribute_list.js";
import NoteListWidget from "../widgets/note_list.js";
import SearchDefinitionWidget from "../widgets/ribbon_widgets/search_definition.js";
import SqlResultWidget from "../widgets/sql_result.js";
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import ImagePropertiesWidget from "../widgets/ribbon_widgets/image_properties.js";
import NotePropertiesWidget from "../widgets/ribbon_widgets/note_properties.js";
import NoteIconWidget from "../widgets/note_icon.js";
import SearchResultWidget from "../widgets/search_result.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import SpacerWidget from "../widgets/spacer.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import BasicPropertiesWidget from "../widgets/ribbon_widgets/basic_properties.js";
import NoteInfoWidget from "../widgets/ribbon_widgets/note_info_widget.js";
import BookPropertiesWidget from "../widgets/ribbon_widgets/book_properties.js";
import NoteMapRibbonWidget from "../widgets/ribbon_widgets/note_map.js";
import NotePathsWidget from "../widgets/ribbon_widgets/note_paths.js";
import SimilarNotesWidget from "../widgets/ribbon_widgets/similar_notes.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import EditButton from "../widgets/floating_buttons/edit_button.js";
import EditedNotesWidget from "../widgets/ribbon_widgets/edited_notes.js";
import ShowTocWidgetButton from "../widgets/buttons/show_toc_widget_button.js";
import ShowHighlightsListWidgetButton from "../widgets/buttons/show_highlights_list_widget_button.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import FindWidget from "../widgets/find.js";
import TocWidget from "../widgets/toc.js"; import TocWidget from "../widgets/toc.js";
import HighlightsListWidget from "../widgets/highlights_list.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RevisionsButton from "../widgets/buttons/revisions_button.js";
import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
import ApiLogWidget from "../widgets/api_log.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
import CloseZenButton from "../widgets/close_zen_button.js";
import type { AppContext } from "../components/app_context.js"; import type { AppContext } from "../components/app_context.js";
import type { WidgetsByParent } from "../services/bundle.js"; import type { WidgetsByParent } from "../services/bundle.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js"; import SwitchSplitOrientationButton from "../widgets/floating_buttons/switch_layout_button.js";
import utils from "../services/utils.js"; import ToggleReadOnlyButton from "../widgets/floating_buttons/toggle_read_only_button.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js"; import PngExportButton from "../widgets/floating_buttons/png_export_button.js";
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
import { applyModals } from "./layout_commons.js";
export default class DesktopLayout { export default class DesktopLayout {
@@ -78,9 +107,9 @@ export default class DesktopLayout {
new FlexContainer("row") new FlexContainer("row")
.class("tab-row-container") .class("tab-row-container")
.child(new FlexContainer("row").id("tab-row-left-spacer")) .child(new FlexContainer("row").id("tab-row-left-spacer"))
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />) .optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
.child(new TabRowWidget().class("full-width")) .child(new TabRowWidget().class("full-width"))
.optChild(customTitleBarButtons, <TitleBarButtons />) .optChild(customTitleBarButtons, new TitleBarButtonsWidget())
.css("height", "40px") .css("height", "40px")
.css("background-color", "var(--launcher-pane-background-color)") .css("background-color", "var(--launcher-pane-background-color)")
.setParent(appContext) .setParent(appContext)
@@ -101,7 +130,7 @@ export default class DesktopLayout {
new FlexContainer("column") new FlexContainer("column")
.id("rest-pane") .id("rest-pane")
.css("flex-grow", "1") .css("flex-grow", "1")
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px")) .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
.child( .child(
new FlexContainer("row") new FlexContainer("row")
.filling() .filling()
@@ -122,33 +151,69 @@ export default class DesktopLayout {
.css("min-height", "50px") .css("min-height", "50px")
.css("align-items", "center") .css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }") .cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />) .child(new NoteIconWidget())
.child(<NoteTitleWidget />) .child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1)) .child(new SpacerWidget(0, 1))
.child(<MovePaneButton direction="left" />) .child(new MovePaneButton(true))
.child(<MovePaneButton direction="right" />) .child(new MovePaneButton(false))
.child(<ClosePaneButton />) .child(new ClosePaneButton())
.child(<CreatePaneButton />) .child(new CreatePaneButton())
) )
.child(<Ribbon />) .child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget()) .child(new WatchedFileUpdateStatusWidget())
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />) .child(
new FloatingButtons()
.child(new RefreshButton())
.child(new SwitchSplitOrientationButton())
.child(new ToggleReadOnlyButton())
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new GeoMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new PngExportButton())
.child(new BacklinksWidget())
.child(new ContextualHelpButton())
.child(new HideFloatingButtonsButton())
)
.child( .child(
new ScrollingContainer() new ScrollingContainer()
.filling() .filling()
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
.child(new PromotedAttributesWidget()) .child(new PromotedAttributesWidget())
.child(<SqlTableSchemas />) .child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget()) .child(new NoteDetailWidget())
.child(<NoteList media="screen" />) .child(new NoteListWidget(false))
.child(<SearchResult />) .child(new SearchResultWidget())
.child(<SqlResults />) .child(new SqlResultWidget())
.child(<ScrollPadding />) .child(new ScrollPaddingWidget())
) )
.child(<ApiLog />) .child(new ApiLogWidget())
.child(new FindWidget()) .child(new FindWidget())
.child( .child(
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
@@ -167,11 +232,11 @@ export default class DesktopLayout {
) )
) )
) )
.child(<CloseZenModeButton />) .child(new CloseZenButton())
// Desktop-specific dialogs. // Desktop-specific dialogs.
.child(<PasswordNoteSetDialog />) .child(new PasswordNoteSetDialog())
.child(<UploadAttachmentsDialog />); .child(new UploadAttachmentsDialog());
applyModals(rootContainer); applyModals(rootContainer);
return rootContainer; return rootContainer;
@@ -181,18 +246,14 @@ export default class DesktopLayout {
let launcherPane; let launcherPane;
if (isHorizontal) { if (isHorizontal) {
launcherPane = new FlexContainer("row") launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
.css("height", "53px")
.class("horizontal")
.child(new LauncherContainer(true))
.child(<GlobalMenu isHorizontalLayout={true} />);
} else { } else {
launcherPane = new FlexContainer("column") launcherPane = new FlexContainer("column")
.css("width", "53px") .css("width", "53px")
.class("vertical") .class("vertical")
.child(<GlobalMenu isHorizontalLayout={false} />) .child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false)) .child(new LauncherContainer(false))
.child(<LeftPaneToggle isHorizontalLayout={false} />); .child(new LeftPaneToggleWidget(false));
} }
launcherPane.id("launcher-pane"); launcherPane.id("launcher-pane");

View File

@@ -24,49 +24,46 @@ import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
import FlexContainer from "../widgets/containers/flex_container.js"; import FlexContainer from "../widgets/containers/flex_container.js";
import NoteIconWidget from "../widgets/note_icon"; import NoteIconWidget from "../widgets/note_icon.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import NoteTitleWidget from "../widgets/note_title.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import NoteDetailWidget from "../widgets/note_detail.js"; import NoteDetailWidget from "../widgets/note_detail.js";
import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; import NoteListWidget from "../widgets/note_list.js";
import NoteTitleWidget from "../widgets/note_title.jsx";
import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
export function applyModals(rootContainer: RootContainer) { export function applyModals(rootContainer: RootContainer) {
rootContainer rootContainer
.child(<BulkActionsDialog />) .child(new BulkActionsDialog())
.child(<AboutDialog />) .child(new AboutDialog())
.child(<HelpDialog />) .child(new HelpDialog())
.child(<RecentChangesDialog />) .child(new RecentChangesDialog())
.child(<BranchPrefixDialog />) .child(new BranchPrefixDialog())
.child(<SortChildNotesDialog />) .child(new SortChildNotesDialog())
.child(<IncludeNoteDialog />) .child(new IncludeNoteDialog())
.child(<NoteTypeChooserDialog />) .child(new NoteTypeChooserDialog())
.child(<JumpToNoteDialog />) .child(new JumpToNoteDialog())
.child(<AddLinkDialog />) .child(new AddLinkDialog())
.child(<CloneToDialog />) .child(new CloneToDialog())
.child(<MoveToDialog />) .child(new MoveToDialog())
.child(<ImportDialog />) .child(new ImportDialog())
.child(<ExportDialog />) .child(new ExportDialog())
.child(<MarkdownImportDialog />) .child(new MarkdownImportDialog())
.child(<ProtectedSessionPasswordDialog />) .child(new ProtectedSessionPasswordDialog())
.child(<RevisionsDialog />) .child(new RevisionsDialog())
.child(<DeleteNotesDialog />) .child(new DeleteNotesDialog())
.child(<InfoDialog />) .child(new InfoDialog())
.child(<ConfirmDialog />) .child(new ConfirmDialog())
.child(<PromptDialog />) .child(new PromptDialog())
.child(<IncorrectCpuArchDialog />) .child(new IncorrectCpuArchDialog())
.child(new PopupEditorDialog() .child(new PopupEditorDialog()
.child(new FlexContainer("row") .child(new FlexContainer("row")
.class("title-row") .class("title-row")
.css("align-items", "center") .css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }") .cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />) .child(new NoteIconWidget())
.child(<NoteTitleWidget />)) .child(new NoteTitleWidget()))
.child(<StandaloneRibbonAdapter component={FormattingToolbar} />) .child(new ClassicEditorToolbar())
.child(new PromotedAttributesWidget()) .child(new PromotedAttributesWidget())
.child(new NoteDetailWidget()) .child(new NoteDetailWidget())
.child(<NoteList media="screen" displayOnlyCollections />)) .child(new NoteListWidget(true)))
.child(<CallToActionDialog />);
} }

View File

@@ -0,0 +1,181 @@
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js";
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import FilePropertiesWidget from "../widgets/ribbon_widgets/file_properties.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import EditButton from "../widgets/floating_buttons/edit_button.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
import NoteListWidget from "../widgets/note_list.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import type AppContext from "../components/app_context.js";
import TabRowWidget from "../widgets/tab_row.js";
import RefreshButton from "../widgets/floating_buttons/refresh_button.js";
import MobileEditorToolbar from "../widgets/ribbon_widgets/mobile_editor_toolbar.js";
import { applyModals } from "./layout_commons.js";
const MOBILE_CSS = `
<style>
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-left: 0.5em;
padding-right: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
</style>`;
const FANCYTREE_CSS = `
<style>
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-left: 10px;
}
.fancytree-custom-icon {
font-size: 2em;
}
.fancytree-title {
font-size: 1.5em;
margin-left: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
.fancytree-node .fancytree-expander:before {
font-size: 2em !important;
}
span.fancytree-expander {
width: 24px !important;
margin-right: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-right: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
</style>`;
export default class MobileLayout {
getRootWidget(appContext: typeof AppContext) {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(
new FlexContainer("row")
.filling()
.id("mobile-rest-container")
.child(
new SidebarContainer("tree", "column")
.class("d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
.id("mobile-sidebar-wrapper")
.css("max-height", "100%")
.css("padding-left", "0")
.css("padding-right", "0")
.css("contain", "content")
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
)
.child(
new ScreenContainer("detail", "column")
.id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.child(
new FlexContainer("row")
.contentSized()
.css("font-size", "larger")
.css("align-items", "center")
.child(new ToggleSidebarButtonWidget().contentSized())
.child(new NoteTitleWidget().contentSized().css("position", "relative").css("padding-left", "0.5em"))
.child(new MobileDetailMenuWidget(true).contentSized())
)
.child(new SharedInfoWidget())
.child(
new FloatingButtons()
.child(new RefreshButton())
.child(new EditButton())
.child(new RelationMapButtons())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new PromotedAttributesWidget())
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new NoteDetailWidget())
.child(new NoteListWidget(false))
.child(new FilePropertiesWidget().css("font-size", "smaller"))
)
.child(new MobileEditorToolbar())
)
)
.child(
new FlexContainer("column")
.contentSized()
.id("mobile-bottom-bar")
.child(new TabRowWidget().css("height", "40px"))
.child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
);
applyModals(rootContainer);
return rootContainer;
}
}

View File

@@ -1,200 +0,0 @@
import { applyModals } from "./layout_commons.js";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
import NoteDetailWidget from "../widgets/note_detail.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import ContentHeader from "../widgets/containers/content-header.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import type AppContext from "../components/app_context.js";
const MOBILE_CSS = `
<style>
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
</style>`;
const FANCYTREE_CSS = `
<style>
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-inline-start: 10px;
}
.fancytree-custom-icon {
font-size: 2em;
}
.fancytree-title {
font-size: 1.5em;
margin-inline-start: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
.fancytree-node .fancytree-expander:before {
font-size: 2em !important;
}
span.fancytree-expander {
width: 24px !important;
margin-inline-end: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-inline-end: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
</style>`;
export default class MobileLayout {
getRootWidget(appContext: typeof AppContext) {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(
new FlexContainer("row")
.filling()
.id("mobile-rest-container")
.child(
new SidebarContainer("tree", "column")
.class("d-md-flex d-lg-flex d-xl-flex col-12 col-sm-5 col-md-4 col-lg-3 col-xl-3")
.id("mobile-sidebar-wrapper")
.css("max-height", "100%")
.css("padding-inline-start", "0")
.css("padding-inline-end", "0")
.css("contain", "content")
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
)
.child(
new ScreenContainer("detail", "row")
.id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.child(
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.contentSized()
.css("font-size", "larger")
.css("align-items", "center")
.child(<ToggleSidebarButton />)
.child(<NoteTitleWidget />)
.child(<MobileDetailMenu />)
)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(new PromotedAttributesWidget())
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfoWidget />)
)
.child(new NoteDetailWidget())
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
)
.child(<MobileEditorToolbar />)
)
)
)
.child(
new FlexContainer("column")
.contentSized()
.id("mobile-bottom-bar")
.child(new TabRowWidget().css("height", "40px"))
.child(new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(<GlobalMenuWidget isHorizontalLayout />)
.id("launcher-pane"))
)
.child(<CloseZenModeButton />);
applyModals(rootContainer);
return rootContainer;
}
}
function FilePropertiesWrapper() {
const { note } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} />}
</div>
);
}

View File

@@ -1,3 +1,5 @@
import "./stylesheets/bootstrap.scss";
// @ts-ignore - module = undefined // @ts-ignore - module = undefined
// Required for correct loading of scripts in Electron // Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;} if (typeof module === 'object') {window.module = module; module = undefined;}

View File

@@ -1,8 +1,6 @@
import { KeyboardActionNames } from "@triliumnext/commons"; import keyboardActionService from "../services/keyboard_actions.js";
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
import note_tooltip from "../services/note_tooltip.js"; import note_tooltip from "../services/note_tooltip.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { should } from "vitest";
export interface ContextMenuOptions<T> { export interface ContextMenuOptions<T> {
x: number; x: number;
@@ -15,13 +13,8 @@ export interface ContextMenuOptions<T> {
onHide?: () => void; onHide?: () => void;
} }
export interface MenuSeparatorItem { interface MenuSeparatorItem {
kind: "separator"; title: "----";
}
export interface MenuHeader {
title: string;
kind: "header";
} }
export interface MenuItemBadge { export interface MenuItemBadge {
@@ -33,11 +26,6 @@ export interface MenuCommandItem<T> {
title: string; title: string;
command?: T; command?: T;
type?: string; type?: string;
/**
* The icon to display in the menu item.
*
* If not set, no icon is displayed and the item will appear shifted slightly to the left if there are other items with icons. To avoid this, use `bx bx-empty`.
*/
uiIcon?: string; uiIcon?: string;
badges?: MenuItemBadge[]; badges?: MenuItemBadge[];
templateNoteId?: string; templateNoteId?: string;
@@ -45,13 +33,12 @@ export interface MenuCommandItem<T> {
handler?: MenuHandler<T>; handler?: MenuHandler<T>;
items?: MenuItem<T>[] | null; items?: MenuItem<T>[] | null;
shortcut?: string; shortcut?: string;
keyboardShortcut?: KeyboardActionNames;
spellingSuggestion?: string; spellingSuggestion?: string;
checked?: boolean; checked?: boolean;
columns?: number; columns?: number;
} }
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem | MenuHeader; export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void; export type MenuHandler<T> = (item: MenuCommandItem<T>, e: JQuery.MouseDownEvent<HTMLElement, undefined, HTMLElement, HTMLElement>) => void;
export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent; export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEvent;
@@ -150,57 +137,20 @@ class ContextMenu {
this.$widget this.$widget
.css({ .css({
display: "block", display: "block",
top, top: top,
left left: left
}) })
.addClass("show"); .addClass("show");
} }
addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[], multicolumn = false) { addItems($parent: JQuery<HTMLElement>, items: MenuItem<any>[]) {
let $group = $parent; // The current group or parent element to which items are being appended for (const item of items) {
let shouldStartNewGroup = false; // If true, the next item will start a new group
let shouldResetGroup = false; // If true, the next item will be the last one from the group
for (let index = 0; index < items.length; index++) {
const item = items[index];
if (!item) { if (!item) {
continue; continue;
} }
// If the current item is a header, start a new group. This group will contain the if (item.title === "----") {
// header and the next item that follows the header. $parent.append($("<div>").addClass("dropdown-divider"));
if ("kind" in item && item.kind === "header") {
if (multicolumn && !shouldResetGroup) {
shouldStartNewGroup = true;
}
}
// If the next item is a separator, start a new group. This group will contain the
// current item, the separator, and the next item after the separator.
const nextItem = (index < items.length - 1) ? items[index + 1] : null;
if (multicolumn && nextItem && "kind" in nextItem && nextItem.kind === "separator") {
if (!shouldResetGroup) {
shouldStartNewGroup = true;
} else {
shouldResetGroup = true; // Continue the current group
}
}
// Create a new group to avoid column breaks before and after the seaparator / header.
// This is a workaround for Firefox not supporting break-before / break-after: avoid
// for columns.
if (shouldStartNewGroup) {
$group = $("<div class='dropdown-no-break'>");
$parent.append($group);
shouldStartNewGroup = false;
}
if ("kind" in item && item.kind === "separator") {
$group.append($("<div>").addClass("dropdown-divider"));
shouldResetGroup = true; // End the group after the next item
} else if ("kind" in item && item.kind === "header") {
$group.append($("<h6>").addClass("dropdown-header").text(item.title));
shouldResetGroup = true;
} else { } else {
const $icon = $("<span>"); const $icon = $("<span>");
@@ -230,23 +180,7 @@ class ContextMenu {
} }
} }
if ("keyboardShortcut" in item && item.keyboardShortcut) { if ("shortcut" in item && item.shortcut) {
const shortcuts = getActionSync(item.keyboardShortcut).effectiveShortcuts;
if (shortcuts) {
const allShortcuts: string[] = [];
for (const effectiveShortcut of shortcuts) {
allShortcuts.push(effectiveShortcut.split("+")
.map(key => `<kbd>${key}</kbd>`)
.join("+"));
}
if (allShortcuts.length) {
const container = $("<span>").addClass("keyboard-shortcut");
container.append($(allShortcuts.join(",")));
$link.append(container);
}
}
} else if ("shortcut" in item && item.shortcut) {
$link.append($("<kbd>").text(item.shortcut)); $link.append($("<kbd>").text(item.shortcut));
} }
@@ -302,24 +236,16 @@ class ContextMenu {
$link.addClass("dropdown-toggle"); $link.addClass("dropdown-toggle");
const $subMenu = $("<ul>").addClass("dropdown-menu"); const $subMenu = $("<ul>").addClass("dropdown-menu");
const hasColumns = !!item.columns && item.columns > 1; if (!this.isMobile && item.columns) {
if (!this.isMobile && hasColumns) { $subMenu.css("column-count", item.columns);
$subMenu.css("column-count", item.columns!);
} }
this.addItems($subMenu, item.items, hasColumns); this.addItems($subMenu, item.items);
$item.append($subMenu); $item.append($subMenu);
} }
$group.append($item); $parent.append($item);
// After adding a menu item, if the previous item was a separator or header,
// reset the group so that the next item will be appended directly to the parent.
if (shouldResetGroup) {
$group = $parent;
shouldResetGroup = false;
};
} }
} }
} }

View File

@@ -37,7 +37,7 @@ function setupContextMenu() {
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
}); });
items.push({ kind: "separator" }); items.push({ title: `----` });
} }
if (params.isEditable) { if (params.isEditable) {
@@ -112,7 +112,7 @@ function setupContextMenu() {
// Replace the placeholder with the real search keyword. // Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText)); let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" }); items.push({ title: "----" });
items.push({ items.push({
title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }), title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }),

View File

@@ -45,16 +45,16 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null, isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-script-launcher"), command: "addScriptLauncher", uiIcon: "bx bx-code-curly" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null, isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-custom-widget"), command: "addWidgetLauncher", uiIcon: "bx bx-customize" } : null,
isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null, isVisibleRoot || isAvailableRoot ? { title: t("launcher_context_menu.add-spacer"), command: "addSpacerLauncher", uiIcon: "bx bx-dots-horizontal" } : null,
isVisibleRoot || isAvailableRoot ? { kind: "separator" } : null, isVisibleRoot || isAvailableRoot ? { title: "----" } : null,
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null, isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null, isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
isVisibleItem || isAvailableItem ? { kind: "separator" } : null, isVisibleItem || isAvailableItem ? { title: "----" } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem }, { title: `${t("launcher_context_menu.duplicate-launcher")}`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
{ title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted }, { title: `${t("launcher_context_menu.delete")}`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
{ kind: "separator" }, { title: "----" },
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset } { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset }
]; ];

View File

@@ -13,8 +13,6 @@ import type NoteTreeWidget from "../widgets/note_tree.js";
import type FAttachment from "../entities/fattachment.js"; import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js"; import type { SelectMenuItemEventListener } from "../components/events.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
// TODO: Deduplicate once client/server is well split. // TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse { interface ConvertToAttachmentResponse {
@@ -25,7 +23,7 @@ let lastTargetNode: HTMLElement | null = null;
// This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator, // This will include all commands that implement ContextMenuCommandData, but it will not work if it additional options are added via the `|` operator,
// so they need to be added manually. // so they need to be added manually.
export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog" | "searchInSubtree"; export type TreeCommandNames = FilteredCommandNames<ContextMenuCommandData> | "openBulkActionsDialog";
export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> { export default class TreeContextMenu implements SelectMenuItemEventListener<TreeCommandNames> {
private treeWidget: NoteTreeWidget; private treeWidget: NoteTreeWidget;
@@ -63,11 +61,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
// the only exception is when the only selected note is the one that was right-clicked, then // the only exception is when the only selected note is the one that was right-clicked, then
// it's clear what the user meant to do. // it's clear what the user meant to do.
const selNodes = this.treeWidget.getSelectedNodes(); const selNodes = this.treeWidget.getSelectedNodes();
const selectedNotes = await froca.getNotes(selNodes.map(node => node.data.noteId));
if (note && !selectedNotes.includes(note)) selectedNotes.push(note);
const isArchived = selectedNotes.every(note => note.isArchived);
const canToggleArchived = !selectedNotes.some(note => note.isArchived !== isArchived);
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node); const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search"; const notSearch = note?.type !== "search";
@@ -76,29 +69,27 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch; const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
const items: (MenuItem<TreeCommandNames> | null)[] = [ const items: (MenuItem<TreeCommandNames> | null)[] = [
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes }, { title: `${t("tree-context-menu.open-in-a-new-tab")}`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes }, { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes }, { title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
isHoisted isHoisted
? null ? null
: { : {
title: `${t("tree-context-menu.hoist-note")}`, title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`,
command: "toggleNoteHoisting", command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up", uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch enabled: noSelectedNotes && notSearch
}, },
!isHoisted || !isNotRoot !isHoisted || !isNotRoot
? null ? null
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" }, : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ kind: "separator" }, { title: "----" },
{ {
title: t("tree-context-menu.insert-note-after"), title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`,
command: "insertNoteAfter", command: "insertNoteAfter",
keyboardShortcut: "createNoteAfter",
uiIcon: "bx bx-plus", uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null, items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp, enabled: insertNoteAfterEnabled && noSelectedNotes && notOptionsOrHelp,
@@ -106,22 +97,21 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
}, },
{ {
title: t("tree-context-menu.insert-child-note"), title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`,
command: "insertChildNote", command: "insertChildNote",
keyboardShortcut: "createNoteInto",
uiIcon: "bx bx-plus", uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null, items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp, enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
columns: 2 columns: 2
}, },
{ kind: "separator" }, { title: "----" },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes }, { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ kind: "separator" }, { title: "----" },
{ {
title: t("tree-context-menu.advanced"), title: t("tree-context-menu.advanced"),
@@ -130,52 +120,54 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
items: [ items: [
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true }, { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
{ kind: "separator" }, { title: "----" },
{ {
title: t("tree-context-menu.edit-branch-prefix"), title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`,
command: "editBranchPrefix", command: "editBranchPrefix",
keyboardShortcut: "editBranchPrefix",
uiIcon: "bx bx-rename", uiIcon: "bx bx-rename",
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
}, },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp }, { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
{ kind: "separator" },
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ {
title: t("tree-context-menu.sort-by"), title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
command: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
},
{ title: "----" },
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{
title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`,
command: "sortChildNotes", command: "sortChildNotes",
keyboardShortcut: "sortChildNotes",
uiIcon: "bx bx-sort-down", uiIcon: "bx bx-sort-down",
enabled: noSelectedNotes && notSearch enabled: noSelectedNotes && notSearch
}, },
{ kind: "separator" }, { title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true }, { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp } { title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
] ]
}, },
{ kind: "separator" }, { title: "----" },
{ {
title: t("tree-context-menu.cut"), title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`,
command: "cutNotesToClipboard", command: "cutNotesToClipboard",
keyboardShortcut: "cutNotesToClipboard",
uiIcon: "bx bx-cut", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch enabled: isNotRoot && !isHoisted && parentNotSearch
}, },
{ title: t("tree-context-menu.copy-clone"), command: "copyNotesToClipboard", keyboardShortcut: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted }, { title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy", enabled: isNotRoot && !isHoisted },
{ {
title: t("tree-context-menu.paste-into"), title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`,
command: "pasteNotesFromClipboard", command: "pasteNotesFromClipboard",
keyboardShortcut: "pasteNotesFromClipboard",
uiIcon: "bx bx-paste", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes
}, },
@@ -188,71 +180,32 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
}, },
{ {
title: t("tree-context-menu.move-to"), title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`,
command: "moveNotesTo", command: "moveNotesTo",
keyboardShortcut: "moveNotesTo",
uiIcon: "bx bx-transfer", uiIcon: "bx bx-transfer",
enabled: isNotRoot && !isHoisted && parentNotSearch enabled: isNotRoot && !isHoisted && parentNotSearch
}, },
{ title: t("tree-context-menu.clone-to"), command: "cloneNotesTo", keyboardShortcut: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted }, { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
{ {
title: t("tree-context-menu.duplicate"), title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
command: "duplicateSubtree",
keyboardShortcut: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
},
{
title: !isArchived ? t("tree-context-menu.archive") : t("tree-context-menu.unarchive"),
uiIcon: !isArchived ? "bx bx-archive" : "bx bx-archive-out",
enabled: canToggleArchived,
handler: () => {
if (!selectedNotes.length) return;
if (selectedNotes.length == 1) {
const note = selectedNotes[0];
if (!isArchived) {
attributes.addLabel(note.noteId, "archived");
} else {
attributes.removeOwnedLabelByName(note, "archived");
}
} else {
const noteIds = selectedNotes.map(note => note.noteId);
if (!isArchived) {
executeBulkActions(noteIds, [{
name: "addLabel", labelName: "archived"
}]);
} else {
executeBulkActions(noteIds, [{
name: "deleteLabel", labelName: "archived"
}]);
}
}
}
},
{
title: t("tree-context-menu.delete"),
command: "deleteNotes", command: "deleteNotes",
keyboardShortcut: "deleteNotes",
uiIcon: "bx bx-trash destructive-action-icon", uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp enabled: isNotRoot && !isHoisted && parentNotSearch && notOptionsOrHelp
}, },
{ kind: "separator" }, { title: "----" },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp }, { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export", enabled: notSearch && noSelectedNotes && notOptionsOrHelp },
{ kind: "separator" }, { title: "----" },
{ {
title: t("tree-context-menu.search-in-subtree"), title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`,
command: "searchInSubtree", command: "searchInSubtree",
keyboardShortcut: "searchInSubtree",
uiIcon: "bx bx-search", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes enabled: notSearch && noSelectedNotes
} }

View File

@@ -1,6 +1,7 @@
import appContext from "./components/app_context.js"; import appContext from "./components/app_context.js";
import noteAutocompleteService from "./services/note_autocomplete.js"; import noteAutocompleteService from "./services/note_autocomplete.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import "./stylesheets/bootstrap.scss";
import "boxicons/css/boxicons.min.css"; import "boxicons/css/boxicons.min.css";
import "autocomplete.js/index_jquery.js"; import "autocomplete.js/index_jquery.js";

View File

@@ -1,157 +0,0 @@
@import "boxicons/css/boxicons.min.css";
:root {
--print-font-size: 11pt;
--ck-content-color-image-caption-background: transparent !important;
}
html,
body {
width: 100%;
height: 100%;
color: black;
}
@page {
margin: 2cm;
}
.note-list-widget.full-height,
.note-list-widget.full-height .note-list-widget-content {
height: unset !important;
}
.component {
contain: none !important;
}
body[data-note-type="text"] .ck-content {
font-size: var(--print-font-size);
text-align: justify;
}
.ck-content figcaption {
font-style: italic;
}
.ck-content a {
text-decoration: none;
}
.ck-content a:not([href^="#root/"]) {
text-decoration: underline;
color: #374a75;
}
.ck-content .todo-list__label * {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
@supports selector(.todo-list__label__description:has(*)) and (height: 1lh) {
.ck-content .todo-list__label__description {
/* The percentage of the line height that the check box occupies */
--box-ratio: 0.75;
/* The size of the gap between the check box and the caption */
--box-text-gap: 0.25em;
--box-size: calc(1lh * var(--box-ratio));
--box-vert-offset: calc((1lh - var(--box-size)) / 2);
display: inline-block;
padding-inline-start: calc(var(--box-size) + var(--box-text-gap));
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-blank-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5C3.89%2c3 3%2c3.89 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5C21%2c3.89 20.1%2c3 19%2c3M19%2c5V19H5V5H19Z' /%3e%3c/svg%3e");
background-position: 0 var(--box-vert-offset);
background-size: var(--box-size);
background-repeat: no-repeat;
}
.ck-content .todo-list__label:has(input[type="checkbox"]:checked) .todo-list__label__description {
/* Source: https://pictogrammers.com/library/mdi/icon/checkbox-outline/ */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3e%3cpath d='M19%2c3H5A2%2c2 0 0%2c0 3%2c5V19A2%2c2 0 0%2c0 5%2c21H19A2%2c2 0 0%2c0 21%2c19V5A2%2c2 0 0%2c0 19%2c3M19%2c5V19H5V5H19M10%2c17L6%2c13L7.41%2c11.58L10%2c14.17L16.59%2c7.58L18%2c9' /%3e%3c/svg%3e");
}
.ck-content .todo-list__label input[type="checkbox"] {
display: none !important;
}
}
/* #region Footnotes */
.footnote-reference a,
.footnote-back-link a {
text-decoration: none !important;
}
li.footnote-item {
position: relative;
width: fit-content;
}
.ck-content .footnote-back-link {
margin-right: 0.25em;
}
.ck-content .footnote-content {
display: inline-block;
width: unset;
}
/* #endregion */
/* #region Widows and orphans */
p,
blockquote {
widows: 4;
orphans: 4;
}
pre > code {
widows: 6;
orphans: 6;
overflow: auto;
white-space: pre-wrap !important;
}
h1,
h2,
h3,
h4,
h5,
h6 {
page-break-after: avoid;
break-after: avoid;
}
/* #endregion */
/* #region Tables */
.table thead th,
.table td,
.table th {
/* Fix center vertical alignment of table cells */
vertical-align: middle;
}
pre {
box-shadow: unset !important;
border: 0.75pt solid gray !important;
border-radius: 2pt !important;
}
th,
span[style] {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* #endregion */
/* #region Page breaks */
.page-break {
page-break-after: always;
break-after: always;
}
.page-break > *,
.page-break::after {
display: none !important;
}
/* #endregion */

View File

@@ -1,132 +0,0 @@
import FNote from "./entities/fnote";
import { render } from "preact";
import { CustomNoteList } from "./widgets/collections/NoteList";
import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import content_renderer from "./services/content_renderer";
interface RendererProps {
note: FNote;
onReady: () => void;
}
async function main() {
const notePath = window.location.hash.substring(1);
const noteId = notePath.split("/").at(-1);
if (!noteId) return;
await import("./print.css");
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
render(<App note={note} noteId={noteId} />, document.body);
}
function App({ note, noteId }: { note: FNote | null | undefined, noteId: string }) {
const sentReadyEvent = useRef(false);
const onReady = useCallback(() => {
if (sentReadyEvent.current) return;
window.dispatchEvent(new Event("note-ready"));
window._noteReady = true;
sentReadyEvent.current = true;
}, []);
const props: RendererProps | undefined | null = note && { note, onReady };
if (!note || !props) return <Error404 noteId={noteId} />
useLayoutEffect(() => {
document.body.dataset.noteType = note.type;
}, [ note ]);
return (
<>
{note.type === "book"
? <CollectionRenderer {...props} />
: <SingleNoteRenderer {...props} />
}
</>
);
}
function SingleNoteRenderer({ note, onReady }: RendererProps) {
const containerRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
async function load() {
if (note.type === "text") {
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 });
});
})
);
// Check custom CSS.
await loadCustomCss(note);
}
load().then(() => requestAnimationFrame(onReady))
}, [ note ]);
return <>
<h1>{note.title}</h1>
<main ref={containerRef} />
</>;
}
function CollectionRenderer({ note, onReady }: RendererProps) {
return <CustomNoteList
isEnabled
note={note}
notePath={note.getBestNotePath().join("/")}
ntxId="print"
highlightedTokens={null}
media="print"
onReady={async () => {
await loadCustomCss(note);
onReady();
}}
/>;
}
function Error404({ noteId }: { noteId: string }) {
return (
<main>
<p>The note you are trying to print could not be found.</p>
<small>{noteId}</small>
</main>
)
}
async function loadCustomCss(note: FNote) {
const printCssNotes = await note.getRelationTargets("printCss");
let loadPromises: JQueryPromise<void>[] = [];
for (const printCssNote of printCssNotes) {
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
const linkEl = document.createElement("link");
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
linkEl.rel = "stylesheet";
const promise = $.Deferred();
loadPromises.push(promise.promise());
linkEl.onload = () => promise.resolve();
document.head.appendChild(linkEl);
}
await Promise.allSettled(loadPromises);
}
main();

View File

@@ -1,15 +1,5 @@
import $ from "jquery"; import $ from "jquery";
async function loadBootstrap() {
if (document.body.dir === "rtl") {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
(window as any).$ = $; (window as any).$ = $;
(window as any).jQuery = $; (window as any).jQuery = $;
await loadBootstrap();
$("body").show(); $("body").show();

View File

@@ -79,19 +79,7 @@ async function renderAttributes(attributes: FAttribute[], renderIsInheritable: b
return $container; return $container;
} }
const HIDDEN_ATTRIBUTES = [ const HIDDEN_ATTRIBUTES = ["originalFileName", "fileSize", "template", "inherit", "cssClass", "iconClass", "pageSize", "viewType", "geolocation", "docName"];
"originalFileName",
"fileSize",
"template",
"inherit",
"cssClass",
"iconClass",
"pageSize",
"viewType",
"geolocation",
"docName",
"webViewSrc"
];
async function renderNormalAttributes(note: FNote) { async function renderNormalAttributes(note: FNote) {
const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes();

View File

@@ -2,7 +2,6 @@ import server from "./server.js";
import froca from "./froca.js"; import froca from "./froca.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js"; import type { AttributeRow } from "./load_results.js";
import { AttributeType } from "@triliumnext/commons";
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) { async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, { await server.put(`notes/${noteId}/attribute`, {
@@ -13,12 +12,11 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
}); });
} }
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) { export async function setLabel(noteId: string, name: string, value: string = "") {
await server.put(`notes/${noteId}/set-attribute`, { await server.put(`notes/${noteId}/set-attribute`, {
type: "label", type: "label",
name: name, name: name,
value: value, value: value
isInheritable
}); });
} }
@@ -26,14 +24,6 @@ async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`); await server.remove(`notes/${noteId}/attributes/${attributeId}`);
} }
export async function removeOwnedAttributesByNameOrType(note: FNote, type: AttributeType, name: string) {
for (const attr of note.getOwnedAttributes()) {
if (attr.type === type && attr.name === name) {
await server.remove(`notes/${note.noteId}/attributes/${attr.attributeId}`);
}
}
}
/** /**
* Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e. * Removes a label identified by its name from the given note, if it exists. Note that the label must be owned, i.e.
* it will not remove inherited attributes. * it will not remove inherited attributes.
@@ -61,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
* @param value the value of the attribute to set. * @param value the value of the attribute to set.
*/ */
export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) { export async function setAttribute(note: FNote, type: "label" | "relation", name: string, value: string | null | undefined) {
if (value !== null && value !== undefined) { if (value) {
// Create or update the attribute. // Create or update the attribute.
await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value }); await server.put(`notes/${note.noteId}/set-attribute`, { type, name, value });
} else { } else {

View File

@@ -95,15 +95,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
} }
} }
/** async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false) {
* Shows the delete confirmation screen
*
* @param branchIdsToDelete the list of branch IDs to delete.
* @param forceDeleteAllClones whether to check by default the "Delete also all clones" checkbox.
* @param moveToParent whether to automatically go to the parent note path after a succesful delete. Usually makes sense if deleting the active note(s).
* @returns promise that returns false if the operation was cancelled or there was nothing to delete, true if the operation succeeded.
*/
async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = false, moveToParent = true) {
branchIdsToDelete = filterRootNote(branchIdsToDelete); branchIdsToDelete = filterRootNote(branchIdsToDelete);
if (branchIdsToDelete.length === 0) { if (branchIdsToDelete.length === 0) {
@@ -118,12 +110,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
return false; return false;
} }
if (moveToParent) { try {
try { await activateParentNotePath();
await activateParentNotePath(); } catch (e) {
} catch (e) { console.error(e);
console.error(e);
}
} }
const taskId = utils.randomString(10); const taskId = utils.randomString(10);
@@ -210,7 +200,7 @@ function makeToast(id: string, message: string): ToastOptions {
} }
ws.subscribeToMessages(async (message) => { ws.subscribeToMessages(async (message) => {
if (!("taskType" in message) || message.taskType !== "deleteNotes") { if (message.taskType !== "deleteNotes") {
return; return;
} }
@@ -228,7 +218,7 @@ ws.subscribeToMessages(async (message) => {
}); });
ws.subscribeToMessages(async (message) => { ws.subscribeToMessages(async (message) => {
if (!("taskType" in message) || message.taskType !== "undeleteNotes") { if (message.taskType !== "undeleteNotes") {
return; return;
} }

View File

@@ -15,10 +15,8 @@ import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import toast from "./toast.js";
import { BulkAction } from "@triliumnext/commons";
export const ACTION_GROUPS = [ const ACTION_GROUPS = [
{ {
title: t("bulk_actions.labels"), title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction] actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
@@ -91,17 +89,6 @@ function parseActions(note: FNote) {
.filter((action) => !!action); .filter((action) => !!action);
} }
export async function executeBulkActions(targetNoteIds: string[], actions: BulkAction[], includeDescendants = false) {
await server.post("bulk-action/execute", {
noteIds: targetNoteIds,
includeDescendants,
actions
});
await ws.waitForMaxKnownEntityChangeId();
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
}
export default { export default {
addAction, addAction,
parseActions, parseActions,

View File

@@ -1,295 +0,0 @@
import { ActionKeyboardShortcut } from "@triliumnext/commons";
import appContext, { type CommandNames } from "../components/app_context.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import { t, translationsInitializedPromise } from "./i18n.js";
import keyboardActions from "./keyboard_actions.js";
import utils from "./utils.js";
export interface CommandDefinition {
id: string;
name: string;
description?: string;
icon?: string;
shortcut?: string;
commandName?: CommandNames;
handler?: () => Promise<unknown> | null | undefined | void;
aliases?: string[];
source?: "manual" | "keyboard-action";
/** Reference to the original keyboard action for scope checking. */
keyboardAction?: ActionKeyboardShortcut;
}
class CommandRegistry {
private commands: Map<string, CommandDefinition> = new Map();
private aliases: Map<string, string> = new Map();
constructor() {
this.loadCommands();
}
private async loadCommands() {
await translationsInitializedPromise;
this.registerDefaultCommands();
await this.loadKeyboardActionsAsync();
}
private registerDefaultCommands() {
this.register({
id: "export-note",
name: t("command_palette.export_note_title"),
description: t("command_palette.export_note_description"),
icon: "bx bx-export",
handler: () => {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
appContext.triggerCommand("showExportDialog", {
notePath,
defaultType: "single"
});
}
}
});
this.register({
id: "show-attachments",
name: t("command_palette.show_attachments_title"),
description: t("command_palette.show_attachments_description"),
icon: "bx bx-paperclip",
handler: () => appContext.triggerCommand("showAttachments")
});
// Special search commands with custom logic
this.register({
id: "search-notes",
name: t("command_palette.search_notes_title"),
description: t("command_palette.search_notes_description"),
icon: "bx bx-search",
handler: () => appContext.triggerCommand("searchNotes", {})
});
this.register({
id: "search-in-subtree",
name: t("command_palette.search_subtree_title"),
description: t("command_palette.search_subtree_description"),
icon: "bx bx-search-alt",
handler: () => {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
appContext.triggerCommand("searchInSubtree", { notePath });
}
}
});
this.register({
id: "show-search-history",
name: t("command_palette.search_history_title"),
description: t("command_palette.search_history_description"),
icon: "bx bx-history",
handler: () => appContext.triggerCommand("showSearchHistory")
});
this.register({
id: "show-launch-bar",
name: t("command_palette.configure_launch_bar_title"),
description: t("command_palette.configure_launch_bar_description"),
icon: "bx bx-sidebar",
handler: () => appContext.triggerCommand("showLaunchBarSubtree")
});
}
private async loadKeyboardActionsAsync() {
try {
const actions = await keyboardActions.getActions();
this.registerKeyboardActions(actions);
} catch (error) {
console.error("Failed to load keyboard actions:", error);
}
}
private registerKeyboardActions(actions: ActionKeyboardShortcut[]) {
for (const action of actions) {
// Skip actions that we've already manually registered
if (this.commands.has(action.actionName)) {
continue;
}
// Skip actions that don't have a description (likely separators)
if (!action.description) {
continue;
}
// Skip Electron-only actions if not in Electron environment
if (action.isElectronOnly && !utils.isElectron()) {
continue;
}
// Skip actions that should not appear in the command palette
if (action.ignoreFromCommandPalette) {
continue;
}
// Get the primary shortcut (first one in the list)
const primaryShortcut = action.effectiveShortcuts?.[0];
let name = action.friendlyName;
if (action.scope === "note-tree") {
name = t("command_palette.tree-action-name", { name: action.friendlyName });
}
// Create a command definition from the keyboard action
const commandDef: CommandDefinition = {
id: action.actionName,
name,
description: action.description,
icon: action.iconClass,
shortcut: primaryShortcut ? this.formatShortcut(primaryShortcut) : undefined,
commandName: action.actionName as CommandNames,
source: "keyboard-action",
keyboardAction: action
};
this.register(commandDef);
}
}
private formatShortcut(shortcut: string): string {
// Convert electron accelerator format to display format
return shortcut
.replace(/CommandOrControl/g, 'Ctrl')
.replace(/\+/g, ' + ');
}
register(command: CommandDefinition) {
this.commands.set(command.id, command);
// Register aliases
if (command.aliases) {
for (const alias of command.aliases) {
this.aliases.set(alias.toLowerCase(), command.id);
}
}
}
getCommand(id: string): CommandDefinition | undefined {
return this.commands.get(id);
}
getAllCommands(): CommandDefinition[] {
const commands = Array.from(this.commands.values());
// Sort commands by name
commands.sort((a, b) => a.name.localeCompare(b.name));
return commands;
}
searchCommands(query: string): CommandDefinition[] {
const normalizedQuery = query.toLowerCase();
const results: { command: CommandDefinition; score: number }[] = [];
for (const command of this.commands.values()) {
let score = 0;
// Exact match on name
if (command.name.toLowerCase() === normalizedQuery) {
score = 100;
}
// Name starts with query
else if (command.name.toLowerCase().startsWith(normalizedQuery)) {
score = 80;
}
// Name contains query
else if (command.name.toLowerCase().includes(normalizedQuery)) {
score = 60;
}
// Description contains query
else if (command.description?.toLowerCase().includes(normalizedQuery)) {
score = 40;
}
// Check aliases
else if (command.aliases?.some(alias => alias.toLowerCase().includes(normalizedQuery))) {
score = 50;
}
if (score > 0) {
results.push({ command, score });
}
}
// Sort by score (highest first) and then by name
results.sort((a, b) => {
if (a.score !== b.score) {
return b.score - a.score;
}
return a.command.name.localeCompare(b.command.name);
});
return results.map(r => r.command);
}
async executeCommand(commandId: string) {
const command = this.getCommand(commandId);
if (!command) {
console.error(`Command not found: ${commandId}`);
return;
}
// Execute custom handler if provided
if (command.handler) {
await command.handler();
return;
}
// Handle keyboard action with scope-aware execution
if (command.keyboardAction && command.commandName) {
if (command.keyboardAction.scope === "note-tree") {
this.executeWithNoteTreeFocus(command.commandName);
} else if (command.keyboardAction.scope === "text-detail") {
this.executeWithTextDetail(command.commandName);
} else {
appContext.triggerCommand(command.commandName, {
ntxId: appContext.tabManager.activeNtxId
});
}
return;
}
// Fallback for commands without keyboard action reference
if (command.commandName) {
appContext.triggerCommand(command.commandName, {
ntxId: appContext.tabManager.activeNtxId
});
return;
}
console.error(`Command ${commandId} has no handler or commandName`);
}
private executeWithNoteTreeFocus(actionName: CommandNames) {
const tree = document.querySelector(".tree-wrapper") as HTMLElement;
if (!tree) {
return;
}
const treeComponent = appContext.getComponentByEl(tree) as NoteTreeWidget;
const activeNode = treeComponent.getActiveNode();
treeComponent.triggerCommand(actionName, {
ntxId: appContext.tabManager.activeNtxId,
node: activeNode
});
}
private async executeWithTextDetail(actionName: CommandNames) {
const typeWidget = await appContext.tabManager.getActiveContext()?.getTypeWidget();
if (!typeWidget) {
return;
}
typeWidget.triggerCommand(actionName, {
ntxId: appContext.tabManager.activeNtxId
});
}
}
const commandRegistry = new CommandRegistry();
export default commandRegistry;

View File

@@ -23,13 +23,11 @@ interface Options {
tooltip?: boolean; tooltip?: boolean;
trim?: boolean; trim?: boolean;
imageHasZoom?: boolean; imageHasZoom?: boolean;
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
noChildrenList?: boolean;
} }
const CODE_MIME_TYPES = new Set(["application/json"]); const CODE_MIME_TYPES = new Set(["application/json"]);
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) { async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
options = Object.assign( options = Object.assign(
{ {
@@ -44,7 +42,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
const $renderedContent = $('<div class="rendered-content">'); const $renderedContent = $('<div class="rendered-content">');
if (type === "text" || type === "book") { if (type === "text" || type === "book") {
await renderText(entity, $renderedContent, options); await renderText(entity, $renderedContent);
} else if (type === "code") { } else if (type === "code") {
await renderCode(entity, $renderedContent); await renderCode(entity, $renderedContent);
} else if (["image", "canvas", "mindMap"].includes(type)) { } else if (["image", "canvas", "mindMap"].includes(type)) {
@@ -67,9 +65,6 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button)); $renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
} else if (entity instanceof FNote) { } else if (entity instanceof FNote) {
$renderedContent
.css("display", "flex")
.css("flex-direction", "column");
$renderedContent.append( $renderedContent.append(
$("<div>") $("<div>")
.css("display", "flex") .css("display", "flex")
@@ -77,33 +72,8 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
.css("align-items", "center") .css("align-items", "center")
.css("height", "100%") .css("height", "100%")
.css("font-size", "500%") .css("font-size", "500%")
.css("flex-grow", "1")
.append($("<span>").addClass(entity.getIcon())) .append($("<span>").addClass(entity.getIcon()))
); );
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
const $footer = $("<footer>")
.addClass("webview-footer");
const $openButton = $(`
<button class="file-open btn btn-primary" type="button">
<span class="bx bx-link-external"></span>
${t("content_renderer.open_externally")}
</button>
`)
.appendTo($footer)
.on("click", () => {
const webViewSrc = entity.getLabelValue("webViewSrc");
if (webViewSrc) {
if (utils.isElectron()) {
const electron = utils.dynamicRequire("electron");
electron.shell.openExternal(webViewSrc);
} else {
window.open(webViewSrc, '_blank', 'noopener,noreferrer');
}
}
});
$footer.appendTo($renderedContent);
}
} }
if (entity instanceof FNote) { if (entity instanceof FNote) {
@@ -116,7 +86,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
}; };
} }
async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: Options = {}) { async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
// entity must be FNote // entity must be FNote
const blob = await note.getBlob(); const blob = await note.getBlob();
@@ -137,7 +107,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
} }
await formatCodeBlocks($renderedContent); await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) { } else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note); await renderChildrenList($renderedContent, note);
} }
} }
@@ -258,19 +228,8 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
</button> </button>
`); `);
$downloadButton.on("click", (e) => { $downloadButton.on("click", () => openService.downloadFileNote(entity.noteId));
e.stopPropagation(); $openButton.on("click", () => openService.openNoteExternally(entity.noteId, entity.mime));
openService.downloadFileNote(entity.noteId)
});
$openButton.on("click", async (e) => {
const iconEl = $openButton.find("> .bx");
iconEl.removeClass("bx bx-link-external");
iconEl.addClass("bx bx-loader spin");
e.stopPropagation();
await openService.openNoteExternally(entity.noteId, entity.mime)
iconEl.removeClass("bx bx-loader spin");
iconEl.addClass("bx bx-link-external");
});
// open doesn't work for protected notes since it works through a browser which isn't in protected session // open doesn't work for protected notes since it works through a browser which isn't in protected session
$openButton.toggle(!entity.isProtected); $openButton.toggle(!entity.isProtected);

View File

@@ -1,39 +1,21 @@
import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color";
const registeredClasses = new Set<string>(); const registeredClasses = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables function createClassForColor(color: string | null) {
if (!color?.trim()) {
return "";
}
const lightThemeColorMaxLightness = readCssVar( const normalizedColorName = color.replace(/[^a-z0-9]/gi, "");
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
const darkThemeColorMinLightness = readCssVar( if (!normalizedColorName.trim()) {
document.documentElement, return "";
"tree-item-dark-theme-min-color-lightness" }
).asNumber(50);
function createClassForColor(colorString: string | null) { const className = `color-${normalizedColorName}`;
if (!colorString?.trim()) return "";
const color = parseColor(colorString);
if (!color) return "";
const className = `color-${color.hex().substring(1)}`;
if (!registeredClasses.has(className)) { if (!registeredClasses.has(className)) {
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!, // make the active fancytree selector more specific than the normal color setting
darkThemeColorMinLightness!); $("head").append(`<style>.${className}, span.fancytree-active.${className} { color: ${color} !important; }</style>`);
$("head").append(`<style>
.${className}, span.fancytree-active.${className} {
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
--dark-theme-custom-color: ${adjustedColor.darkThemeColor};
--custom-color-hue: ${getHue(color) ?? 'unset'};
}
</style>`);
registeredClasses.add(className); registeredClasses.add(className);
} }
@@ -41,41 +23,6 @@ function createClassForColor(colorString: string | null) {
return className; return className;
} }
function parseColor(color: string) {
try {
return Color(color);
} catch (ex) {
console.error(ex);
}
}
/**
* Returns a pair of colors — one optimized for light themes and the other for dark themes, derived
* from the specified color to maintain sufficient contrast with each theme.
* The adjustment is performed by limiting the colors lightness in the CIELAB color space,
* according to the lightThemeMaxLightness and darkThemeMinLightness parameters.
*/
function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: number, darkThemeMinLightness: number) {
const labColor = color.lab();
const lightness = labColor.l();
// For the light theme, limit the maximum lightness
const lightThemeColor = labColor.l(Math.min(lightness, lightThemeMaxLightness)).hex();
// For the dark theme, limit the minimum lightness
const darkThemeColor = labColor.l(Math.max(lightness, darkThemeMinLightness)).hex();
return {lightThemeColor, darkThemeColor};
}
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
function getHue(color: ColorInstance) {
const hslColor = color.hsl();
if (hslColor.saturationl() > 0) {
return hslColor.hue();
}
}
export default { export default {
createClassForColor createClassForColor
}; };

View File

@@ -41,14 +41,8 @@ async function info(message: string) {
return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res }));
} }
/**
* Displays a confirmation dialog with the given message.
*
* @param message the message to display in the dialog.
* @returns A promise that resolves to true if the user confirmed, false otherwise.
*/
async function confirm(message: string) { async function confirm(message: string) {
return new Promise<boolean>((res) => return new Promise((res) =>
appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{ appContext.triggerCommand("showConfirmDialog", <ConfirmWithMessageOptions>{
message, message,
callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed) callback: (x: false | ConfirmDialogOptions) => res(x && x.confirmed)
@@ -60,7 +54,7 @@ async function confirmDeleteNoteBoxWithNote(title: string) {
return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res })); return new Promise<ConfirmDialogResult | undefined>((res) => appContext.triggerCommand("showConfirmDeleteNoteBoxWithNoteDialog", { title, callback: res }));
} }
export async function prompt(props: PromptDialogOptions) { async function prompt(props: PromptDialogOptions) {
return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res })); return new Promise<string | null>((res) => appContext.triggerCommand("showPromptDialog", { ...props, callback: res }));
} }

View File

@@ -48,6 +48,6 @@ function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works. // Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20"); docNameValue = docNameValue.replaceAll(" ", "%20");
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath; const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`; return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
} }

View File

@@ -1,8 +1,16 @@
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { OpenedFileUpdateStatus } from "@triliumnext/commons";
const fileModificationStatus: Record<string, Record<string, OpenedFileUpdateStatus>> = { // TODO: Deduplicate
interface Message {
type: string;
entityType: string;
entityId: string;
lastModifiedMs: number;
filePath: string;
}
const fileModificationStatus: Record<string, Record<string, Message>> = {
notes: {}, notes: {},
attachments: {} attachments: {}
}; };
@@ -31,7 +39,7 @@ function ignoreModification(entityType: string, entityId: string) {
delete fileModificationStatus[entityType][entityId]; delete fileModificationStatus[entityType][entityId];
} }
ws.subscribeToMessages(async message => { ws.subscribeToMessages(async (message: Message) => {
if (message.type !== "openedFileUpdated") { if (message.type !== "openedFileUpdated") {
return; return;
} }

View File

@@ -40,23 +40,20 @@ class FrocaImpl implements Froca {
constructor() { constructor() {
this.initializedPromise = this.loadInitialTree(); this.initializedPromise = this.loadInitialTree();
this.#clear();
} }
async loadInitialTree() { async loadInitialTree() {
const resp = await server.get<SubtreeResponse>("tree"); const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session // clear the cache only directly before adding new content which is important for e.g., switching to protected session
this.#clear();
this.addResp(resp);
}
#clear() {
this.notes = {}; this.notes = {};
this.branches = {}; this.branches = {};
this.attributes = {}; this.attributes = {};
this.attachments = {}; this.attachments = {};
this.blobPromises = {}; this.blobPromises = {};
this.addResp(resp);
} }
async loadSubTree(subTreeNoteId: string) { async loadSubTree(subTreeNoteId: string) {

View File

@@ -8,7 +8,6 @@ import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js"; import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { default as FNote, FNoteRow } from "../entities/fnote.js"; import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js"; import type { EntityChange } from "../server_types.js";
import type { OptionNames } from "@triliumnext/commons";
async function processEntityChanges(entityChanges: EntityChange[]) { async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges); const loadResults = new LoadResults(entityChanges);
@@ -31,14 +30,13 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
continue; // only noise continue; // only noise
} }
options.set(attributeEntity.name as OptionNames, attributeEntity.value); options.set(attributeEntity.name, attributeEntity.value);
loadResults.addOption(attributeEntity.name as OptionNames);
loadResults.addOption(attributeEntity.name);
} else if (ec.entityName === "attachments") { } else if (ec.entityName === "attachments") {
processAttachment(loadResults, ec); processAttachment(loadResults, ec);
} else if (ec.entityName === "blobs") { } else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
// NOOP - these entities are handled at the backend level and don't require frontend processing // NOOP - these entities are handled at the backend level and don't require frontend processing
} else if (ec.entityName === "etapi_tokens") {
loadResults.hasEtapiTokenChanges = true;
} else { } else {
throw new Error(`Unknown entityName '${ec.entityName}'`); throw new Error(`Unknown entityName '${ec.entityName}'`);
} }
@@ -79,7 +77,9 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate(); noteAttributeCache.invalidate();
} }
const appContext = (await import("../components/app_context.js")).default; // TODO: Remove after porting the file
// @ts-ignore
const appContext = (await import("../components/app_context.js")).default as any;
await appContext.triggerEvent("entitiesReloaded", { loadResults }); await appContext.triggerEvent("entitiesReloaded", { loadResults });
} }
} }

View File

@@ -21,7 +21,6 @@ import dayjs from "dayjs";
import type NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
import type NoteDetailWidget from "../widgets/note_detail.js"; import type NoteDetailWidget from "../widgets/note_detail.js";
import type Component from "../components/component.js"; import type Component from "../components/component.js";
import { formatLogMessage } from "@triliumnext/commons";
/** /**
* A whole number * A whole number
@@ -456,7 +455,7 @@ export interface Api {
/** /**
* Log given message to the log pane in UI * Log given message to the log pane in UI
*/ */
log(message: string | object): void; log(message: string): void;
} }
/** /**
@@ -697,7 +696,7 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.log = (message) => { this.log = (message) => {
const { noteId } = this.startNote; const { noteId } = this.startNote;
message = `${utils.now()}: ${formatLogMessage(message)}`; message = `${utils.now()}: ${message}`;
console.log(`Script ${noteId}: ${message}`); console.log(`Script ${noteId}: ${message}`);

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { describe, expect, it } from "vitest";
describe("i18n", () => { describe("i18n", () => {
it("translations are valid JSON", () => { it("translations are valid JSON", () => {
for (const locale of LOCALES) { for (const locale of LOCALES) {
if (locale.contentOnly || locale.id === "en_rtl") { if (locale.contentOnly) {
continue; continue;
} }

View File

@@ -3,21 +3,14 @@ import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend"; import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js"; import server from "./server.js";
import type { Locale } from "@triliumnext/commons"; import type { Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null; let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export let translationsInitializedPromise = $.Deferred();
export async function initLocale() { export async function initLocale() {
const locale = (options.get("locale") as string) || "en"; const locale = (options.get("locale") as string) || "en";
locales = await server.get<Locale[]>("options/locales"); locales = await server.get<Locale[]>("options/locales");
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({ await i18next.use(i18nextHttpBackend).init({
lng: locale, lng: locale,
fallbackLng: "en", fallbackLng: "en",
@@ -26,8 +19,6 @@ export async function initLocale() {
}, },
returnEmptyString: false returnEmptyString: false
}); });
translationsInitializedPromise.resolve();
} }
export function getAvailableLocales() { export function getAvailableLocales() {

View File

@@ -1,7 +1,7 @@
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toastService, { showError } from "./toast.js"; import toastService, { showError } from "./toast.js";
export function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try { try {
$imageWrapper.attr("contenteditable", "true"); $imageWrapper.attr("contenteditable", "true");
selectImage($imageWrapper.get(0)); selectImage($imageWrapper.get(0));

View File

@@ -4,7 +4,6 @@ import ws from "./ws.js";
import utils from "./utils.js"; import utils from "./utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import { WebSocketMessage } from "@triliumnext/commons";
type BooleanLike = boolean | "true" | "false"; type BooleanLike = boolean | "true" | "false";
@@ -67,7 +66,7 @@ function makeToast(id: string, message: string): ToastOptions {
} }
ws.subscribeToMessages(async (message) => { ws.subscribeToMessages(async (message) => {
if (!("taskType" in message) || message.taskType !== "importNotes") { if (message.taskType !== "importNotes") {
return; return;
} }
@@ -88,8 +87,8 @@ ws.subscribeToMessages(async (message) => {
} }
}); });
ws.subscribeToMessages(async (message: WebSocketMessage) => { ws.subscribeToMessages(async (message) => {
if (!("taskType" in message) || message.taskType !== "importAttachments") { if (message.taskType !== "importAttachments") {
return; return;
} }

View File

@@ -1,44 +0,0 @@
import { NoteType } from "@triliumnext/commons";
import FNote from "../entities/fnote";
import { ViewTypeOptions } from "../widgets/collections/interface";
export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
canvas: null,
code: null,
contentWidget: null,
doc: null,
file: null,
image: null,
launcher: null,
mermaid: "s1aBHPd79XYj",
mindMap: null,
noteMap: null,
relationMap: null,
render: null,
search: null,
text: null,
webView: null,
aiChat: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {
list: "mULW0Q3VojwY",
grid: "8QqnMzx393bx",
calendar: "xWbu3jpNWapp",
table: "2FvYrpmOXm29",
geoMap: "81SGnPGMk7Xc",
board: "CtBQqbwXDx1w",
presentation: null
};
export function getHelpUrlForNote(note: FNote | null | undefined) {
if (note && note.type !== "book" && byNoteType[note.type]) {
return byNoteType[note.type];
} else if (note?.hasLabel("calendarRoot")) {
return "l0tKav7yLHGF";
} else if (note?.hasLabel("textSnippet")) {
return "pwc194wlRzcH";
} else if (note && note.type === "book") {
return byBookType[note.getAttributeValue("label", "viewType") as ViewTypeOptions ?? ""]
}
}

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