mirror of
https://github.com/zadam/trilium.git
synced 2025-11-09 23:05:51 +01:00
Compare commits
1 Commits
v0.99.4
...
fix/resolv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0a55fec60 |
@@ -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
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -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
|
|
||||||
|
|||||||
27
.github/actions/build-electron/action.yml
vendored
27
.github/actions/build-electron/action.yml
vendored
@@ -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
|
|
||||||
|
|||||||
6
.github/actions/build-server/action.yml
vendored
6
.github/actions/build-server/action.yml
vendored
@@ -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: |
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
2
.github/actions/report-size/action.yml
vendored
2
.github/actions/report-size/action.yml
vendored
@@ -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
|
||||||
|
|||||||
18
.github/workflows/checks.yml
vendored
18
.github/workflows/checks.yml
vendored
@@ -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 }}"
|
|
||||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -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}}"
|
||||||
|
|||||||
78
.github/workflows/deploy-docs.yml
vendored
78
.github/workflows/deploy-docs.yml
vendored
@@ -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 }}
|
|
||||||
49
.github/workflows/dev.yml
vendored
49
.github/workflows/dev.yml
vendored
@@ -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
|
||||||
|
|||||||
34
.github/workflows/main-docker.yml
vendored
34
.github/workflows/main-docker.yml
vendored
@@ -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-*
|
||||||
|
|||||||
19
.github/workflows/nightly.yml
vendored
19
.github/workflows/nightly.yml
vendored
@@ -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
|
||||||
|
|||||||
28
.github/workflows/playwright.yml
vendored
28
.github/workflows/playwright.yml
vendored
@@ -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
|
|
||||||
|
|||||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
11
.github/workflows/unblock_signing.yml
vendored
11
.github/workflows/unblock_signing.yml
vendored
@@ -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 }}
|
|
||||||
51
.github/workflows/website.yml
vendored
51
.github/workflows/website.yml
vendored
@@ -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
12
.gitignore
vendored
@@ -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
6
.idea/.gitignore
generated
vendored
Normal 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
15
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
12
.idea/dataSources.xml
generated
Normal 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
4
.idea/encodings.xml
generated
Normal 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
15
.idea/git_toolbox_prj.xml
generated
Normal 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>
|
||||||
11
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
11
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
6
.idea/jsLibraryMappings.xml
generated
Normal 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
9
.idea/jsLinters/jslint.xml
generated
Normal 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
8
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
7
.idea/sqldialects.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
4
.mailmap
4
.mailmap
@@ -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>
|
||||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -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",
|
||||||
|
|||||||
5
.vscode/i18n-ally-custom-framework.yml
vendored
5
.vscode/i18n-ally-custom-framework.yml
vendored
@@ -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
12
.vscode/settings.json
vendored
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
5
.vscode/snippets.code-snippets
vendored
5
.vscode/snippets.code-snippets
vendored
@@ -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
152
CLAUDE.md
@@ -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
141
README.md
@@ -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
|
||||||
|
|
||||||
 
|

|
||||||

|

|
||||||

|

|
||||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
[](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
|
|||||||
|
|
||||||
[](https://repology.org/project/triliumnext/versions)
|
[](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:
|
|
||||||
|
|
||||||
[](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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
15
_regroup/typedoc.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"entryPoints": [
|
||||||
|
"src/services/backend_script_entrypoint.ts",
|
||||||
|
"src/public/app/services/frontend_script_entrypoint.ts"
|
||||||
|
],
|
||||||
|
"plugin": [
|
||||||
|
"typedoc-plugin-missing-exports"
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "html",
|
||||||
|
"path": "./docs/Script API"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "build-docs",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "src/main.ts",
|
|
||||||
"scripts": {
|
|
||||||
"start": "tsx ."
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Elian Doran <contact@eliandoran.me>",
|
|
||||||
"license": "AGPL-3.0-only",
|
|
||||||
"packageManager": "pnpm@10.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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* The backend script API is accessible to code notes with the "JS (backend)" language.
|
|
||||||
*
|
|
||||||
* The entire API is exposed as a single global: {@link api}
|
|
||||||
*
|
|
||||||
* @module Backend Script API
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
|
||||||
* script note on the server side.
|
|
||||||
*
|
|
||||||
* Make sure to keep in line with backend's `script_context.ts`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
|
|
||||||
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
|
|
||||||
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
|
|
||||||
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
|
|
||||||
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
|
|
||||||
export type { BNote };
|
|
||||||
export type { default as BOption } from "../../server/src/becca/entities/boption.js";
|
|
||||||
export type { default as BRecentNote } from "../../server/src/becca/entities/brecent_note.js";
|
|
||||||
export type { default as BRevision } from "../../server/src/becca/entities/brevision.js";
|
|
||||||
|
|
||||||
import BNote from "../../server/src/becca/entities/bnote.js";
|
|
||||||
import BackendScriptApi, { type Api } from "../../server/src/services/backend_script_api.js";
|
|
||||||
|
|
||||||
export type { Api };
|
|
||||||
|
|
||||||
const fakeNote = new BNote();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
|
|
||||||
*/
|
|
||||||
export const api: Api = new BackendScriptApi(fakeNote, {});
|
|
||||||
@@ -1,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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default interface BuildContext {
|
|
||||||
gitRootDir: string;
|
|
||||||
baseDir: string;
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
|
||||||
*
|
|
||||||
* The entire API is exposed as a single global: {@link api}
|
|
||||||
*
|
|
||||||
* @module Frontend Script API
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
|
||||||
* script note.
|
|
||||||
*
|
|
||||||
* Make sure to keep in line with frontend's `script_context.ts`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
|
|
||||||
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
|
|
||||||
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
|
|
||||||
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
|
|
||||||
export type { default as FNote } from "../../client/src/entities/fnote.js";
|
|
||||||
export type { Api } from "../../client/src/services/frontend_script_api.js";
|
|
||||||
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
|
|
||||||
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
|
|
||||||
|
|
||||||
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
export const api: Api = new FrontendScriptApi();
|
|
||||||
@@ -1,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>
|
|
||||||
@@ -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();
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
import BuildContext from "./context";
|
|
||||||
import { join } from "path";
|
|
||||||
|
|
||||||
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
|
|
||||||
// Generate types
|
|
||||||
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });
|
|
||||||
|
|
||||||
for (const config of [ "backend", "frontend" ]) {
|
|
||||||
const outDir = join(baseDir, "script-api", config);
|
|
||||||
execSync(`pnpm typedoc --options typedoc.${config}.json --html "${outDir}"`, {
|
|
||||||
stdio: "inherit"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import BuildContext from "./context";
|
|
||||||
import { join } from "path";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { mkdirSync } from "fs";
|
|
||||||
|
|
||||||
interface BuildInfo {
|
|
||||||
specPath: string;
|
|
||||||
outDir: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DIR_PREFIX = "rest-api";
|
|
||||||
|
|
||||||
const buildInfos: BuildInfo[] = [
|
|
||||||
{
|
|
||||||
// Paths are relative to Git root.
|
|
||||||
specPath: "apps/server/internal.openapi.yaml",
|
|
||||||
outDir: `${DIR_PREFIX}/internal`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
specPath: "apps/server/etapi.openapi.yaml",
|
|
||||||
outDir: `${DIR_PREFIX}/etapi`
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
|
|
||||||
for (const { specPath, outDir } of buildInfos) {
|
|
||||||
const absSpecPath = join(gitRootDir, specPath);
|
|
||||||
const targetDir = join(baseDir, outDir);
|
|
||||||
mkdirSync(targetDir, { recursive: true });
|
|
||||||
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"target": "ES2020",
|
|
||||||
"outDir": "dist",
|
|
||||||
"strict": false,
|
|
||||||
"types": [
|
|
||||||
"node",
|
|
||||||
"express"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"tsBuildInfoFile": "dist/tsconfig.app.tsbuildinfo"
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"../server/src/*.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"eslint.config.js",
|
|
||||||
"eslint.config.cjs",
|
|
||||||
"eslint.config.mjs"
|
|
||||||
],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "../server/tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../desktop/tsconfig.app.json"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../client/tsconfig.app.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "../server"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "../client"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://typedoc.org/schema.json",
|
|
||||||
"name": "Trilium Backend API",
|
|
||||||
"entryPoints": [
|
|
||||||
"src/backend_script_entrypoint.ts"
|
|
||||||
],
|
|
||||||
"plugin": [
|
|
||||||
"typedoc-plugin-missing-exports"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://typedoc.org/schema.json",
|
|
||||||
"name": "Trilium Frontend API",
|
|
||||||
"entryPoints": [
|
|
||||||
"src/frontend_script_entrypoint.ts"
|
|
||||||
],
|
|
||||||
"plugin": [
|
|
||||||
"typedoc-plugin-missing-exports"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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> {}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -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 />);
|
|
||||||
}
|
}
|
||||||
181
apps/client/src/layouts/mobile_layout.ts
Normal file
181
apps/client/src/layouts/mobile_layout.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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 }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 */
|
|
||||||
@@ -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();
|
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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 color’s 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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|
||||||
|
|||||||
28
apps/client/src/services/frontend_script_entrypoint.ts
Normal file
28
apps/client/src/services/frontend_script_entrypoint.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* The front script API is accessible to code notes with the "JS (frontend)" language.
|
||||||
|
*
|
||||||
|
* The entire API is exposed as a single global: {@link api}
|
||||||
|
*
|
||||||
|
* @module Frontend Script API
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file creates the entrypoint for TypeDoc that simulates the context from within a
|
||||||
|
* script note.
|
||||||
|
*
|
||||||
|
* Make sure to keep in line with frontend's `script_context.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type { default as BasicWidget } from "../widgets/basic_widget.js";
|
||||||
|
export type { default as FAttachment } from "../entities/fattachment.js";
|
||||||
|
export type { default as FAttribute } from "../entities/fattribute.js";
|
||||||
|
export type { default as FBranch } from "../entities/fbranch.js";
|
||||||
|
export type { default as FNote } from "../entities/fnote.js";
|
||||||
|
export type { Api } from "./frontend_script_api.js";
|
||||||
|
export type { default as NoteContextAwareWidget } from "../widgets/note_context_aware_widget.js";
|
||||||
|
export type { default as RightPanelWidget } from "../widgets/right_panel_widget.js";
|
||||||
|
|
||||||
|
import FrontendScriptApi, { type Api } from "./frontend_script_api.js";
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
export const api: Api = new FrontendScriptApi();
|
||||||
@@ -20,6 +20,9 @@ function setupGlobs() {
|
|||||||
window.glob.froca = froca;
|
window.glob.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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user