Merge remote-tracking branch 'origin/main' into renovate/csrf-csrf-4.x

This commit is contained in:
Elian Doran 2025-07-26 15:49:50 +03:00
commit a3a3b3cb5c
No known key found for this signature in database
757 changed files with 114965 additions and 12109 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

@ -85,7 +85,7 @@ runs:
APPLE_ID: ${{ env.APPLE_ID }} APPLE_ID: ${{ env.APPLE_ID }}
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: TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }} TRILIUM_ARTIFACT_NAME_HINT: TriliumNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}
run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }} run: pnpm nx --project=desktop electron-forge:make -- --arch=${{ inputs.arch }} --platform=${{ inputs.forge_platform }}
# Add DMG signing step # Add DMG signing step

View File

@ -30,4 +30,4 @@ runs:
mkdir -p upload mkdir -p upload
file=$(find ./apps/server/out -name '*.tar.xz' -print -quit) file=$(find ./apps/server/out -name '*.tar.xz' -print -quit)
name=${{ github.ref_name }} name=${{ github.ref_name }}
cp "$file" "upload/TriliumNextNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz" cp "$file" "upload/TriliumNotes-Server-${name//\//-}-${{ inputs.os }}-${{ inputs.arch }}.tar.xz"

40
.github/instructions/nx.instructions.md vendored Normal file
View File

@ -0,0 +1,40 @@
---
applyTo: '**'
---
// This file is automatically generated by Nx Console
You are in an nx workspace using Nx 21.3.5 and pnpm as the package manager.
You have access to the Nx MCP server and the tools it provides. Use them. Follow these guidelines in order to best help the user:
# General Guidelines
- When answering questions, use the nx_workspace tool first to gain an understanding of the workspace architecture
- For questions around nx configuration, best practices or if you're unsure, use the nx_docs tool to get relevant, up-to-date docs!! Always use this instead of assuming things about nx configuration
- If the user needs help with an Nx configuration or project graph error, use the 'nx_workspace' tool to get any errors
- To help answer questions about the workspace structure or simply help with demonstrating how tasks depend on each other, use the 'nx_visualize_graph' tool
# Generation Guidelines
If the user wants to generate something, use the following flow:
- learn about the nx workspace and any specifics the user needs by using the 'nx_workspace' tool and the 'nx_project_details' tool if applicable
- get the available generators using the 'nx_generators' tool
- decide which generator to use. If no generators seem relevant, check the 'nx_available_plugins' tool to see if the user could install a plugin to help them
- get generator details using the 'nx_generator_schema' tool
- you may use the 'nx_docs' tool to learn more about a specific generator or technology if you're unsure
- decide which options to provide in order to best complete the user's request. Don't make any assumptions and keep the options minimalistic
- open the generator UI using the 'nx_open_generate_ui' tool
- wait for the user to finish the generator
- read the generator log file using the 'nx_read_generator_log' tool
- use the information provided in the log file to answer the user's question or continue with what they were doing
# Running Tasks Guidelines
If the user wants help with tasks or commands (which include keywords like "test", "build", "lint", or other similar actions), use the following flow:
- Use the 'nx_current_running_tasks_details' tool to get the list of tasks (this can include tasks that were completed, stopped or failed).
- If there are any tasks, ask the user if they would like help with a specific task then use the 'nx_current_running_task_output' tool to get the terminal output for that task/command
- Use the terminal output from 'nx_current_running_task_output' to see what's wrong and help the user fix their problem. Use the appropriate tools if necessary
- If the user would like to rerun the task or command, always use `nx run <taskId>` to rerun in the terminal. This will ensure that the task will run in the nx context and will be run the same way it originally executed
- If the task was marked as "continuous" do not offer to rerun the task. This task is already running and the user can see the output in the terminal. You can use 'nx_current_running_task_output' to get the output of the task to verify the output.

17
.github/workflows/checks.yml vendored Normal file
View File

@ -0,0 +1,17 @@
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
with:
dirtyLabel: "merge-conflicts"
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"

View File

@ -13,9 +13,9 @@ name: "CodeQL Advanced"
on: on:
push: push:
branches: [ "develop" ] branches: [ "main" ]
pull_request: pull_request:
branches: [ "develop" ] branches: [ "main" ]
schedule: schedule:
- cron: '20 7 * * 0' - cron: '20 7 * * 0'

View File

@ -1,9 +1,9 @@
name: Dev name: Dev
on: on:
push: push:
branches: [ develop ] branches: [ main ]
pull_request: pull_request:
branches: [ develop ] branches: [ main ]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -12,8 +12,8 @@ concurrency:
env: env:
GHCR_REGISTRY: ghcr.io GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/notes IMAGE_NAME: ${{ github.repository}}
TEST_TAG: ${{ github.repository_owner }}/notes:test TEST_TAG: ${{ github.repository}}:test
permissions: permissions:
pull-requests: write # for PR comments pull-requests: write # for PR comments
@ -39,7 +39,7 @@ jobs:
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
- name: Check affected - name: Check affected
run: pnpm nx affected --verbose -t typecheck build rebuild-deps run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build
test_dev: test_dev:
name: Test development name: Test development
@ -77,6 +77,7 @@ jobs:
- name: Trigger client build - name: Trigger client build
run: pnpm nx run client:build run: pnpm nx run client:build
- name: Send client bundle stats to RelativeCI - name: Send client bundle stats to RelativeCI
if: false
uses: relative-ci/agent-action@v3 uses: relative-ci/agent-action@v3
with: with:
webpackStatsFile: ./apps/client/dist/webpack-stats.json webpackStatsFile: ./apps/client/dist/webpack-stats.json

View File

@ -1,7 +1,7 @@
on: on:
push: push:
branches: branches:
- "develop" - "main"
- "feature/update**" - "feature/update**"
- "feature/server_esm**" - "feature/server_esm**"
paths-ignore: paths-ignore:
@ -14,8 +14,8 @@ on:
env: env:
GHCR_REGISTRY: ghcr.io GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/notes IMAGE_NAME: ${{ github.repository}}
TEST_TAG: ${{ github.repository_owner }}/notes:test TEST_TAG: ${{ github.repository}}:test
permissions: permissions:
contents: read contents: read
@ -83,6 +83,14 @@ jobs:
- name: Run Playwright tests - name: Run Playwright tests
run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e
- name: Upload Playwright trace
if: failure()
uses: actions/upload-artifact@v4
with:
name: Playwright trace (${{ matrix.dockerfile }})
path: test-output/playwright/output
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:

View File

@ -12,7 +12,7 @@ on:
paths: paths:
- .github/actions/build-electron/* - .github/actions/build-electron/*
- .github/workflows/nightly.yml - .github/workflows/nightly.yml
- forge.config.cjs - forge.config.ts
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@ -92,7 +92,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ github.event_name == 'pull_request' }} if: ${{ github.event_name == 'pull_request' }}
with: with:
name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }} name: TriliumNotes ${{ matrix.os.name }} ${{ matrix.arch }}
path: apps/desktop/upload path: apps/desktop/upload
nightly-server: nightly-server:

View File

@ -3,7 +3,7 @@ name: playwright
on: on:
push: push:
branches: branches:
- master - main
pull_request: pull_request:
permissions: permissions:
@ -40,4 +40,4 @@ jobs:
# - run: npx nx-cloud record -- echo Hello World # - run: npx nx-cloud record -- echo Hello World
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
# When you enable task distribution, run the e2e-ci task instead of e2e # When you enable task distribution, run the e2e-ci task instead of e2e
- run: pnpm exec nx affected -t e2e - run: pnpm exec nx affected -t e2e --exclude desktop-e2e

View File

@ -23,7 +23,7 @@ jobs:
shell: bash shell: bash
forge_platform: darwin forge_platform: darwin
- name: linux - name: linux
image: ubuntu-latest image: ubuntu-22.04
shell: bash shell: bash
forge_platform: linux forge_platform: linux
- name: windows - name: windows
@ -120,7 +120,7 @@ jobs:
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
fail_on_unmatched_files: true fail_on_unmatched_files: true
files: upload/*.* files: upload/*.*
discussion_category_name: Announcements discussion_category_name: Releases
make_latest: ${{ !contains(github.ref, 'rc') }} make_latest: ${{ !contains(github.ref, 'rc') }}
prerelease: ${{ contains(github.ref, 'rc') }} prerelease: ${{ contains(github.ref, 'rc') }}
token: ${{ secrets.RELEASE_PAT }} token: ${{ secrets.RELEASE_PAT }}

1
.gitignore vendored
View File

@ -46,3 +46,4 @@ upload
*.tsbuildinfo *.tsbuildinfo
/result /result
.svelte-kit

View File

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

2
.nvmrc
View File

@ -1 +1 @@
22.16.0 22.17.1

View File

@ -1,7 +1,2 @@
_regroup _regroup
_regroup_monorepo _regroup_monorepo
# Asset copying respects .gitignore / .nxignore for some reason.
# See https://github.com/nrwl/nx/issues/20309
!dist
!node_modules

View File

@ -9,6 +9,8 @@
"redhat.vscode-yaml", "redhat.vscode-yaml",
"tobermory.es6-string-html", "tobermory.es6-string-html",
"vitest.explorer", "vitest.explorer",
"yzhang.markdown-all-in-one" "yzhang.markdown-all-in-one",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss"
] ]
} }

8
.vscode/mcp.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"servers": {
"nx-mcp": {
"type": "http",
"url": "http://localhost:9461/mcp"
}
}
}

10
.vscode/settings.json vendored
View File

@ -28,5 +28,13 @@
"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
},
"nxConsole.generateAiAgentRules": true
} }

161
CLAUDE.md Normal file
View File

@ -0,0 +1,161 @@
# 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 NX, 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 nx run server:serve` - Alternative server start command
- `pnpm nx run desktop:serve` - Run desktop Electron app
- `pnpm run server:start-prod` - Run server in production mode
### Building
- `pnpm nx build <project>` - Build specific project (server, client, desktop, etc.)
- `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 nx test <project>` - Run tests for specific project
- `pnpm coverage` - Generate coverage reports
### Linting & Type Checking
- `pnpm nx run <project>:lint` - Lint specific project
- `pnpm nx run <project>:typecheck` - Type check specific project
## 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**:
- `nx.json` - NX workspace 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 NX for monorepo management with build caching
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds

View File

@ -1,13 +1,14 @@
# TriliumNext Notes # Trilium Notes
Donate: ![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran?style=flat-square) ![LiberaPay patrons](https://img.shields.io/liberapay/patrons/ElianDoran?style=flat-square)
![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran?style=flat-square)
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes?style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes?style=flat-square)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total?style=flat-square) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total?style=flat-square)
[![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop&style=flat-square)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [![RelativeCI](https://badges.relative-ci.com/badges/Di5q7dz9daNDZ9UXi0Bp?branch=develop&style=flat-square)](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp)
[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) [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)
TriliumNext 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.
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview: See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
@ -22,7 +23,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) * Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts) * Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional) * UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
* Direct [OpenID and TOTP integration](.docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md") for more secure login * Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server * [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet * [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
@ -119,8 +120,8 @@ To install TriliumNext on your own server (including via Docker from [Dockerhub]
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/Notes.git git clone https://github.com/TriliumNext/Trilium.git
cd Notes cd Trilium
pnpm install pnpm install
pnpm run server:start pnpm run server:start
``` ```
@ -129,8 +130,8 @@ 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/Notes.git git clone https://github.com/TriliumNext/Trilium.git
cd Notes cd Trilium
pnpm install pnpm install
pnpm nx run edit-docs:edit-docs pnpm nx run edit-docs:edit-docs
``` ```
@ -138,8 +139,8 @@ 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/Notes.git git clone https://github.com/TriliumNext/Trilium.git
cd Notes cd Trilium
pnpm install pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32 pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
``` ```
@ -153,7 +154,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide
## 👏 Shoutouts ## 👏 Shoutouts
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team
* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. TriliumNext Notes would not be the same without it. * [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages
* [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) * [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)

View File

@ -91,5 +91,5 @@ async function start() {
} }
// @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs // @TriliumNextTODO sqlInit.dbReady never seems to resolve so program hangs
// see https://github.com/TriliumNext/Notes/issues/1020 // see https://github.com/TriliumNext/Trilium/issues/1020
sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err)); sqlInit.dbReady.then(cls.wrap(start)).catch((err) => console.error(err));

View File

@ -24,7 +24,7 @@ if ! git diff-index --quiet HEAD --; then
exit 1 exit 1
fi fi
BASE_BRANCH=master BASE_BRANCH=main
if [[ "$VERSION" == *"beta"* ]]; then if [[ "$VERSION" == *"beta"* ]]; then
BASE_BRANCH=beta BASE_BRANCH=beta

View File

@ -47,11 +47,3 @@ echo "Tagging commit with $TAG"
git tag $TAG git tag $TAG
git push origin $TAG git push origin $TAG
echo "Updating master"
git fetch
git checkout master
git reset --hard origin/master
git merge origin/develop
git push

View File

@ -25,9 +25,10 @@ stats() {
# Print the number of existing strings on the JSON files for each locale # Print the number of existing strings on the JSON files for each locale
s=$(number_of_keys "${paths[0]}/en/server.json") s=$(number_of_keys "${paths[0]}/en/server.json")
c=$(number_of_keys "${paths[1]}/en/translation.json") c=$(number_of_keys "${paths[1]}/en/translation.json")
echo "| locale |server strings |client strings |" echo "| locale | server strings | client strings |"
echo "|--------|---------------|---------------|" echo "|--------|----------------|----------------|"
echo "| en | ${s} | ${c} |" echo "| en | ${s} | ${c} |"
echo "|--------|----------------|----------------|"
for locale in "${locales[@]}"; do for locale in "${locales[@]}"; do
s=$(number_of_keys "${paths[0]}/${locale}/server.json") s=$(number_of_keys "${paths[0]}/${locale}/server.json")
c=$(number_of_keys "${paths[1]}/${locale}/translation.json") c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
@ -78,7 +79,10 @@ file_path="$(
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
pwd -P pwd -P
)" )"
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/") paths=(
"${file_path}/../../apps/server/src/assets/translations/"
"${file_path}/../../apps/client/src/translations/"
)
locales=(cn de es fr pt_br ro tw) locales=(cn de es fr pt_br ro tw)
if [ $# -eq 1 ]; then if [ $# -eq 1 ]; then

View File

@ -44,7 +44,6 @@ export default tseslint.config(
"dist/*", "dist/*",
"docs/*", "docs/*",
"demo/*", "demo/*",
"libraries/*",
"src/public/app-dist/*", "src/public/app-dist/*",
"src/public/app/doc_notes/*" "src/public/app/doc_notes/*"
] ]

View File

@ -38,7 +38,6 @@ export default [
"dist/*", "dist/*",
"docs/*", "docs/*",
"demo/*", "demo/*",
"libraries/*",
// TriliumNextTODO: check if we want to format packages here as well - for now skipping it // TriliumNextTODO: check if we want to format packages here as well - for now skipping it
"packages/*", "packages/*",
"src/public/app-dist/*", "src/public/app-dist/*",

View File

@ -8,5 +8,5 @@ test("Displays update badge when there is a version available", async ({ page })
await page.getByText(`Version ${expectedVersion} is available,`).click(); await page.getByText(`Version ${expectedVersion} is available,`).click();
const page1 = await page.waitForEvent("popup"); const page1 = await page.waitForEvent("popup");
expect(page1.url()).toBe(`https://github.com/TriliumNext/Notes/releases/tag/v${expectedVersion}`); expect(page1.url()).toBe(`https://github.com/TriliumNext/Trilium/releases/tag/v${expectedVersion}`);
}); });

View File

@ -35,13 +35,13 @@
"chore:generate-openapi": "tsx bin/generate-openapi.js" "chore:generate-openapi": "tsx bin/generate-openapi.js"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.53.0", "@playwright/test": "1.54.1",
"@stylistic/eslint-plugin": "4.4.1", "@stylistic/eslint-plugin": "5.2.2",
"@types/express": "5.0.3", "@types/express": "5.0.3",
"@types/node": "22.15.31", "@types/node": "22.16.5",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.2.3", "@vitest/coverage-v8": "3.2.4",
"eslint": "9.28.0", "eslint": "9.32.0",
"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.4", "jsdoc": "4.0.4",
@ -49,7 +49,7 @@
"rcedit": "4.0.1", "rcedit": "4.0.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"tslib": "2.8.1", "tslib": "2.8.1",
"typedoc": "0.28.5", "typedoc": "0.28.7",
"typedoc-plugin-missing-exports": "4.0.0" "typedoc-plugin-missing-exports": "4.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

5
apps/client/.env Normal file
View File

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

View File

@ -0,0 +1 @@
VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

@ -1,25 +1,26 @@
{ {
"name": "@triliumnext/client", "name": "@triliumnext/client",
"version": "0.94.1", "version": "0.97.1",
"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": "TriliumNext Notes Team", "name": "Trilium Notes Team",
"email": "contact@eliandoran.me", "email": "contact@eliandoran.me",
"url": "https://github.com/TriliumNext/Notes" "url": "https://github.com/TriliumNext/Notes"
}, },
"dependencies": { "dependencies": {
"@eslint/js": "9.28.0", "@eslint/js": "9.32.0",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.17", "@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.17", "@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.17", "@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.17", "@fullcalendar/list": "6.1.18",
"@fullcalendar/multimonth": "6.1.17", "@fullcalendar/multimonth": "6.1.18",
"@fullcalendar/timegrid": "6.1.17", "@fullcalendar/timegrid": "6.1.18",
"@mermaid-js/layout-elk": "0.1.7", "@maplibre/maplibre-gl-leaflet": "0.1.2",
"@mind-elixir/node-menu": "1.0.5", "@mermaid-js/layout-elk": "0.1.8",
"@mind-elixir/node-menu": "5.0.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*", "@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*", "@triliumnext/codemirror": "workspace:*",
@ -27,15 +28,15 @@
"@triliumnext/highlightjs": "workspace:*", "@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*", "@triliumnext/share-theme": "workspace:*",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
"bootstrap": "5.3.6", "bootstrap": "5.3.7",
"boxicons": "2.1.4", "boxicons": "2.1.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0", "debounce": "2.2.0",
"draggabilly": "3.0.0", "draggabilly": "3.0.0",
"force-graph": "1.49.6", "force-graph": "1.50.1",
"globals": "16.2.0", "globals": "16.3.0",
"i18next": "25.2.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-hotkeys": "0.2.2",
@ -46,27 +47,29 @@
"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": "15.0.12", "marked": "16.1.1",
"mermaid": "11.6.0", "mermaid": "11.9.0",
"mind-elixir": "4.6.0", "mind-elixir": "5.0.4",
"normalize.css": "8.0.1", "normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"preact": "10.26.8", "preact": "10.26.9",
"split.js": "1.6.5", "split.js": "1.6.5",
"svg-pan-zoom": "3.6.2", "svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
"vanilla-js-wheel-zoom": "9.0.4" "vanilla-js-wheel-zoom": "9.0.4"
}, },
"devDependencies": { "devDependencies": {
"@ckeditor/ckeditor5-inspector": "4.1.0", "@ckeditor/ckeditor5-inspector": "5.0.0",
"@types/bootstrap": "5.2.10", "@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.32", "@types/jquery": "3.5.32",
"@types/leaflet": "1.9.18", "@types/leaflet": "1.9.20",
"@types/leaflet-gpx": "1.3.7", "@types/leaflet-gpx": "1.3.7",
"@types/mark.js": "8.11.12", "@types/mark.js": "8.11.12",
"@types/tabulator-tables": "6.2.8",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"happy-dom": "18.0.1", "happy-dom": "18.0.1",
"script-loader": "0.7.2", "script-loader": "0.7.2",
"vite-plugin-static-copy": "3.0.0" "vite-plugin-static-copy": "3.1.1"
}, },
"nx": { "nx": {
"name": "client", "name": "client",
@ -75,6 +78,9 @@
"dependsOn": [ "dependsOn": [
"^build" "^build"
] ]
},
"circular-deps": {
"command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
} }
} }
} }

View File

@ -1,424 +0,0 @@
/*
* Remove template code below
*/
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
}
svg {
display: block;
vertical-align: middle;
}
svg {
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {
}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {
}
.button-pill {
padding: 1.5rem 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-console-jetbrains {
margin-top: 2rem;
}
#nx-console-jetbrains:hover {
background-color: rgba(255, 49, 140, 1);
}
#nx-console-jetbrains svg {
color: rgba(255, 49, 140, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@ -1,21 +0,0 @@
import { AppElement } from './app.element';
describe('AppElement', () => {
let app: AppElement;
beforeEach(() => {
app = new AppElement();
});
it('should create successfully', () => {
expect(app).toBeTruthy();
});
it('should have a greeting', () => {
app.connectedCallback();
expect(app.querySelector('h1').innerHTML).toContain(
'Welcome @triliumnext/client'
);
});
});

View File

@ -1,409 +0,0 @@
import './app.element.css';
export class AppElement extends HTMLElement {
public static observedAttributes = [
];
connectedCallback() {
const title = '@triliumnext/client';
this.innerHTML = `
<div class="wrapper">
<div class="container">
<!-- WELCOME -->
<div id="welcome">
<h1>
<span> Hello there, </span>
Welcome ${title} 👋
</h1>
</div>
<!-- HERO -->
<div id="hero" class="rounded">
<div class="text-container">
<h2>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>You&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;s next? </a>
</div>
<div class="logo-container">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z"
/>
</svg>
</div>
</div>
<!-- MIDDLE CONTENT -->
<div id="middle-content">
<div id="learning-materials" class="rounded shadow">
<h2>Learning materials</h2>
<a href="https://nx.dev/getting-started/intro?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<span>
Documentation
<span> Everything is in there </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/blog/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
<span>
Blog
<span> Changelog, features & events </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1" target="_blank" rel="noreferrer" class="list-item-link">
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>YouTube</title>
<path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg>
<span>
YouTube channel
<span> Nx Show, talks & tutorials </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<span>
Interactive tutorials
<span> Create an app, step-by-step </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a href="https://nxplaybook.com/?utm_source=nx-project" target="_blank" rel="noreferrer" class="list-item-link">
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path
d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
/>
</svg>
<span>
Video courses
<span> Nx custom courses </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<div id="other-links">
<a id="nx-console" class="button-pill rounded shadow" href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Visual Studio Code</title>
<path
d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"
/>
</svg>
<span>
Install Nx Console for VSCode
<span>The official VSCode extension for Nx.</span>
</span>
</a>
<a
id="nx-console-jetbrains"
class="button-pill rounded shadow"
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
target="_blank"
rel="noreferrer"
>
<svg
height="48"
width="48"
viewBox="20 20 60 60"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m22.5 22.5h60v60h-60z" />
<g fill="#fff">
<path d="m29.03 71.25h22.5v3.75h-22.5z" />
<path d="m28.09 38 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.44 1.06-1.31v-5.98h2.58v6a3.48 3.48 0 0 1 -.87 2.6 3.56 3.56 0 0 1 -2.57.95 3.84 3.84 0 0 1 -3.34-1.55z" />
<path d="m36 30h7.53v2.19h-5v1.44h4.49v2h-4.42v1.49h5v2.21h-7.6z" />
<path d="m47.23 32.29h-2.8v-2.29h8.21v2.27h-2.81v7.1h-2.6z" />
<path d="m29.13 43.08h4.42a3.53 3.53 0 0 1 2.55.83 2.09 2.09 0 0 1 .6 1.53 2.16 2.16 0 0 1 -1.44 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.61-1.31 2.59-3.55 2.59h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.24c.79 0 1.25-.26 1.25-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86 0-.5-.4-.87-1.27-.87z" />
<path d="m38 43.08h4.1a4.19 4.19 0 0 1 3 1 2.93 2.93 0 0 1 .9 2.19 3 3 0 0 1 -1.93 2.89l2.24 3.27h-3l-1.88-2.84h-.87v2.84h-2.56zm4 4.5c.87 0 1.39-.43 1.39-1.11 0-.75-.54-1.12-1.4-1.12h-1.44v2.26z" />
<path d="m49.59 43h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.69h-2.71zm2.27 5.73-1-2.65-1.06 2.65z" />
<path d="m56.46 43.05h2.6v9.37h-2.6z" />
<path d="m60.06 43.05h2.42l3.37 5v-5h2.57v9.37h-2.26l-3.53-5.14v5.14h-2.57z" />
<path d="m68.86 51 1.45-1.73a4.84 4.84 0 0 0 3 1.13c.71 0 1.08-.24 1.08-.65 0-.4-.31-.6-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.37-3 3.62-3a5.89 5.89 0 0 1 3.86 1.25l-1.26 1.84a4.63 4.63 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .42.32.61 1.63.91 2.14.46 3.44 1.16 3.44 2.91 0 1.91-1.51 3-3.79 3a6.58 6.58 0 0 1 -4.35-1.5z" />
</g>
</svg>
<span>
Install Nx Console for JetBrains
<span>
Available for WebStorm, Intellij IDEA Ultimate and more!
</span>
</span>
</a>
<div id="nx-cloud" class="rounded shadow">
<div>
<svg id="nx-cloud-logo" role="img" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" fill="transparent" viewBox="0 0 24 24">
<path stroke-width="2" d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z" />
<path stroke-width="2" d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z" />
</svg>
<h2>
Nx Cloud
<span>
Enable faster CI & better DX
</span>
</h2>
</div>
<p>
You can activate distributed tasks executions and caching by
running:
</p>
<pre>nx connect</pre>
<a href="https://nx.app/?utm_source=nx-project" target="_blank" rel="noreferrer"> What is Nx Cloud? </a>
</div>
<a id="nx-repo" class="button-pill rounded shadow" href="https://github.com/nrwl/nx?utm_source=nx-project" target="_blank" rel="noreferrer">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
/>
</svg>
<span>
Nx is open source
<span> Love Nx? Give us a star! </span>
</span>
</a>
</div>
</div>
<!-- COMMANDS -->
<div id="commands" class="rounded shadow">
<h2>Next steps</h2>
<p>Here are some things you can do with Nx:</p>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Add UI library
</summary>
<pre><span># Generate UI lib</span>
nx g @nx/angular:lib ui
<span># Add a component</span>
nx g @nx/angular:component ui/src/lib/button</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View interactive project graph
</summary>
<pre>nx graph</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Run affected commands
</summary>
<pre><span># see what&apos;s been affected by changes</span>
nx affected:graph
<span># run tests for current changes</span>
nx affected:test
<span># run e2e tests for current changes</span>
nx affected:e2e</pre>
</details>
</div>
<p id="love">
Carefully crafted with
<svg
fill="currentColor"
stroke="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</p>
</div>
</div>
`;
}
}
customElements.define('triliumnext-root', AppElement);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Client</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<triliumnext-root></triliumnext-root>
</body>
</html>

View File

@ -1 +0,0 @@
import './app/app.element';

View File

@ -1 +0,0 @@
/* You can add global styles to this file, and also import other style files */

View File

@ -1,5 +1,4 @@
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import bundleService from "../services/bundle.js";
import RootCommandExecutor from "./root_command_executor.js"; import RootCommandExecutor from "./root_command_executor.js";
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
import options from "../services/options.js"; import options from "../services/options.js";
@ -28,6 +27,9 @@ import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js"; 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 type { CreateNoteOpts } from "../services/note_create.js";
import { ColumnComponent } from "tabulator-tables";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@ -122,12 +124,14 @@ export type CommandMappings = {
showImportDialog: CommandData & { noteId: string }; showImportDialog: CommandData & { noteId: string };
openNewNoteSplit: NoteCommandData; openNewNoteSplit: NoteCommandData;
openInWindow: NoteCommandData; openInWindow: NoteCommandData;
openInPopup: CommandData & { noteIdOrPath: string; };
openNoteInNewTab: CommandData; openNoteInNewTab: CommandData;
openNoteInNewSplit: CommandData; openNoteInNewSplit: CommandData;
openNoteInNewWindow: CommandData; openNoteInNewWindow: CommandData;
openAboutDialog: CommandData; openAboutDialog: CommandData;
hideFloatingButtons: {}; hideFloatingButtons: {};
hideLeftPane: CommandData; hideLeftPane: CommandData;
showCpuArchWarning: CommandData;
showLeftPane: CommandData; showLeftPane: CommandData;
hoistNote: CommandData & { noteId: string }; hoistNote: CommandData & { noteId: string };
leaveProtectedSession: CommandData; leaveProtectedSession: CommandData;
@ -139,6 +143,7 @@ export type CommandMappings = {
}; };
openInTab: ContextMenuCommandData; openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData; openNoteInSplit: ContextMenuCommandData;
openNoteInPopup: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData; toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData; insertNoteAfter: ContextMenuCommandData;
insertChildNote: ContextMenuCommandData; insertChildNote: ContextMenuCommandData;
@ -260,7 +265,6 @@ export type CommandMappings = {
// Geomap // Geomap
deleteFromMap: { noteId: string }; deleteFromMap: { noteId: string };
openGeoLocation: { noteId: string; event: JQuery.MouseDownEvent };
toggleZenMode: CommandData; toggleZenMode: CommandData;
@ -274,11 +278,27 @@ 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;
}; };
type EventMappings = { type EventMappings = {
@ -467,13 +487,21 @@ export class AppContext extends Component {
this.tabManager.loadTabs(); this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000); setTimeout(() => bundleService.executeStartupBundles(), 2000);
} }
initComponents() { initComponents() {
this.tabManager = new TabManager(); this.tabManager = new TabManager();
this.components = [this.tabManager, new RootCommandExecutor(), new Entrypoints(), new MainTreeExecutors(), new ShortcutComponent()]; this.components = [
this.tabManager,
new RootCommandExecutor(),
new Entrypoints(),
new MainTreeExecutors(),
new ShortcutComponent(),
new StartupChecks()
];
if (utils.isMobile()) { if (utils.isMobile()) {
this.components.push(new MobileScreenSwitcherExecutor()); this.components.push(new MobileScreenSwitcherExecutor());

View File

@ -93,11 +93,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
if (fun) { if (fun) {
return this.callMethod(fun, data); return this.callMethod(fun, data);
} else { } else if (this.parent) {
if (!this.parent) {
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
}
return this.parent.triggerCommand(name, data); return this.parent.triggerCommand(name, data);
} }
} }

View File

@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.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 { closeActiveDialog } from "../services/dialog.js";
export interface SetNoteOpts { export interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); await this.triggerEvent("beforeNoteSwitch", { noteContext: this });
utils.closeActiveDialog(); closeActiveDialog();
this.notePath = resolvedNotePath; this.notePath = resolvedNotePath;
this.viewScope = opts.viewScope; this.viewScope = opts.viewScope;
@ -314,14 +315,39 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
} }
hasNoteList() { hasNoteList() {
return ( const note = this.note;
this.note &&
["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") && if (!note) {
(this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") && return false;
["book", "text", "code"].includes(this.note.type) && }
this.note.mime !== "text/x-sqlite;schema=trilium" &&
!this.note.isLabelTruthy("hideChildrenOverview") if (!["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "")) {
); return false;
}
// Collections must always display a note list, even if no children.
const viewType = note.getLabelValue("viewType") ?? "grid";
if (!["list", "grid"].includes(viewType)) {
return true;
}
if (!note.hasChildren()) {
return false;
}
if (!["book", "text", "code"].includes(note.type)) {
return false;
}
if (note.mime === "text/x-sqlite;schema=trilium") {
return false;
}
if (note.isLabelTruthy("hideChildrenOverview")) {
return false;
}
return true;
} }
async getTextEditor(callback?: GetTextEditorCallback) { async getTextEditor(callback?: GetTextEditorCallback) {

View File

@ -0,0 +1,26 @@
import server from "../services/server";
import Component from "./component";
// TODO: Deduplicate.
interface CpuArchResponse {
isCpuArchMismatch: boolean;
}
export class StartupChecks extends Component {
constructor() {
super();
this.checkCpuArchMismatch();
}
async checkCpuArchMismatch() {
try {
const response = await server.get("system-checks") as CpuArchResponse;
if (response.isCpuArchMismatch) {
this.triggerCommand("showCpuArchWarning", {});
}
} catch (error) {
console.warn("Could not check CPU arch status:", error);
}
}
}

View File

@ -688,7 +688,7 @@ export default class TabManager extends Component {
const titleFragments = [ const titleFragments = [
// it helps to navigate in history if note title is included in the title // it helps to navigate in history if note title is included in the title
await activeNoteContext.getNavigationTitle(), await activeNoteContext.getNavigationTitle(),
"TriliumNext Notes" "Trilium Notes"
].filter(Boolean); ].filter(Boolean);
document.title = titleFragments.join(" - "); document.title = titleFragments.join(" - ");

View File

@ -8,6 +8,7 @@ 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 "./stylesheets/bootstrap.scss";

View File

@ -1,7 +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 ws from "../services/ws.js";
import froca from "../services/froca.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";
@ -28,7 +27,6 @@ const NOTE_TYPE_ICONS = {
doc: "bx bxs-file-doc", doc: "bx bxs-file-doc",
contentWidget: "bx bxs-widget", contentWidget: "bx bxs-widget",
mindMap: "bx bx-sitemap", mindMap: "bx bx-sitemap",
geoMap: "bx bx-map-alt",
aiChat: "bx bx-bot" aiChat: "bx bx-bot"
}; };
@ -37,7 +35,7 @@ const NOTE_TYPE_ICONS = {
* end user. Those types should be used only for checking against, they are * end user. Those types should be used only for checking against, they are
* not for direct use. * not for direct use.
*/ */
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap" | "aiChat"; export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "aiChat";
export interface NotePathRecord { export interface NotePathRecord {
isArchived: boolean; isArchived: boolean;
@ -258,6 +256,20 @@ class FNote {
return this.children; return this.children;
} }
async getSubtreeNoteIds() {
let noteIds: (string | string[])[] = [];
for (const child of await this.getChildNotes()) {
noteIds.push(child.noteId);
noteIds.push(await child.getSubtreeNoteIds());
}
return noteIds.flat();
}
async getSubtreeNotes() {
const noteIds = await this.getSubtreeNoteIds();
return this.froca.getNotes(noteIds);
}
async getChildNotes() { async getChildNotes() {
return await this.froca.getNotes(this.children); return await this.froca.getNotes(this.children);
} }
@ -410,8 +422,8 @@ class FNote {
const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
notePath: path, notePath: path,
isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
isArchived: path.some((noteId) => froca.notes[noteId].isArchived), isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived),
isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"),
isHidden: path.includes("_hidden") isHidden: path.includes("_hidden")
})); }));
@ -982,7 +994,7 @@ class FNote {
continue; continue;
} }
const parentNote = froca.notes[parentNoteId]; const parentNote = this.froca.notes[parentNoteId];
if (!parentNote || parentNote.type === "search") { if (!parentNote || parentNote.type === "search") {
continue; continue;

View File

@ -46,28 +46,7 @@ import SharedInfoWidget from "../widgets/shared_info.js";
import FindWidget from "../widgets/find.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 HighlightsListWidget from "../widgets/highlights_list.js";
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
import AboutDialog from "../widgets/dialogs/about.js";
import HelpDialog from "../widgets/dialogs/help.js";
import RecentChangesDialog from "../widgets/dialogs/recent_changes.js";
import BranchPrefixDialog from "../widgets/dialogs/branch_prefix.js";
import SortChildNotesDialog from "../widgets/dialogs/sort_child_notes.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js"; import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import IncludeNoteDialog from "../widgets/dialogs/include_note.js";
import NoteTypeChooserDialog from "../widgets/dialogs/note_type_chooser.js";
import JumpToNoteDialog from "../widgets/dialogs/jump_to_note.js";
import AddLinkDialog from "../widgets/dialogs/add_link.js";
import CloneToDialog from "../widgets/dialogs/clone_to.js";
import MoveToDialog from "../widgets/dialogs/move_to.js";
import ImportDialog from "../widgets/dialogs/import.js";
import ExportDialog from "../widgets/dialogs/export.js";
import MarkdownImportDialog from "../widgets/dialogs/markdown_import.js";
import ProtectedSessionPasswordDialog from "../widgets/dialogs/protected_session_password.js";
import RevisionsDialog from "../widgets/dialogs/revisions.js";
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
import InfoDialog from "../widgets/dialogs/info.js";
import ConfirmDialog from "../widgets/dialogs/confirm.js";
import PromptDialog from "../widgets/dialogs/prompt.js";
import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js"; import FloatingButtons from "../widgets/floating_buttons/floating_buttons.js";
import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js"; import RelationMapButtons from "../widgets/floating_buttons/relation_map_buttons.js";
import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js"; import SvgExportButton from "../widgets/floating_buttons/svg_export_button.js";
@ -83,7 +62,7 @@ import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_ref
import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js"; import options from "../services/options.js";
import utils, { hasTouchBar } from "../services/utils.js"; import utils from "../services/utils.js";
import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js"; import GeoMapButtons from "../widgets/floating_buttons/geo_map_button.js";
import ContextualHelpButton from "../widgets/floating_buttons/help_button.js"; import ContextualHelpButton from "../widgets/floating_buttons/help_button.js";
import CloseZenButton from "../widgets/close_zen_button.js"; import CloseZenButton from "../widgets/close_zen_button.js";
@ -229,7 +208,7 @@ export default class DesktopLayout {
.child(new PromotedAttributesWidget()) .child(new PromotedAttributesWidget())
.child(new SqlTableSchemasWidget()) .child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget()) .child(new NoteDetailWidget())
.child(new NoteListWidget()) .child(new NoteListWidget(false))
.child(new SearchResultWidget()) .child(new SearchResultWidget())
.child(new SqlResultWidget()) .child(new SqlResultWidget())
.child(new ScrollPaddingWidget()) .child(new ScrollPaddingWidget())

View File

@ -21,6 +21,15 @@ import ConfirmDialog from "../widgets/dialogs/confirm.js";
import RevisionsDialog from "../widgets/dialogs/revisions.js"; import RevisionsDialog from "../widgets/dialogs/revisions.js";
import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js";
import InfoDialog from "../widgets/dialogs/info.js"; import InfoDialog from "../widgets/dialogs/info.js";
import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js";
import PopupEditorDialog from "../widgets/dialogs/popup_editor.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import NoteIconWidget from "../widgets/note_icon.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 NoteListWidget from "../widgets/note_list.js";
export function applyModals(rootContainer: RootContainer) { export function applyModals(rootContainer: RootContainer) {
rootContainer rootContainer
@ -45,4 +54,16 @@ export function applyModals(rootContainer: RootContainer) {
.child(new InfoDialog()) .child(new InfoDialog())
.child(new ConfirmDialog()) .child(new ConfirmDialog())
.child(new PromptDialog()) .child(new PromptDialog())
.child(new IncorrectCpuArchDialog())
.child(new PopupEditorDialog()
.child(new FlexContainer("row")
.class("title-row")
.css("align-items", "center")
.cssBlock(".title-row > * { margin: 5px; }")
.child(new NoteIconWidget())
.child(new NoteTitleWidget()))
.child(new ClassicEditorToolbar())
.child(new PromotedAttributesWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget(true)))
} }

View File

@ -162,7 +162,7 @@ export default class MobileLayout {
.filling() .filling()
.contentSized() .contentSized()
.child(new NoteDetailWidget()) .child(new NoteDetailWidget())
.child(new NoteListWidget()) .child(new NoteListWidget(false))
.child(new FilePropertiesWidget().css("font-size", "smaller")) .child(new FilePropertiesWidget().css("font-size", "smaller"))
) )
.child(new MobileEditorToolbar()) .child(new MobileEditorToolbar())

View File

@ -1,204 +0,0 @@
// Source: https://github.com/codemirror/codemirror5/pull/7080/files
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function (mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function (CodeMirror) {
"use strict";
CodeMirror.defineMode("hcl", function (config) {
var indentUnit = config.indentUnit;
var keywords = {
"resource": true,
"variable": true,
"output": true,
"module": true,
"provider": true,
"data": true,
"locals": true,
"terraform": true,
"if": true,
"else": true,
"for": true,
"foreach": true,
"in": true,
"true": true,
"false": true,
"null": true,
};
var atoms = {
"true": true,
"false": true,
"null": true,
};
var isOperatorChar = /[+\-*&^%:=<>!|\/]/;
var curPunc;
function tokenBase(stream, state) {
var ch = stream.next();
if (ch == '"' || ch == "'" || ch == "`") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
}
if (/[\d\.]/.test(ch)) {
if (ch == ".") {
stream.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/);
} else {
stream.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/);
}
return "number";
}
if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
curPunc = ch;
return null;
}
if (ch == "/") {
if (stream.eat("*")) {
state.tokenize = tokenComment;
return tokenComment(stream, state);
}
if (stream.eat("/")) {
stream.skipToEnd();
return "comment";
}
}
if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
return "operator";
}
stream.eatWhile(/[\w\$_\xa1-\uffff]/);
var cur = stream.current();
if (keywords.propertyIsEnumerable(cur)) {
return "keyword";
}
if (atoms.propertyIsEnumerable(cur)) return "atom";
return "variable";
}
function tokenString(quote) {
return function (stream, state) {
var escaped = false,
next,
end = false;
while ((next = stream.next()) != null) {
if (next == quote && !escaped) {
end = true;
break;
}
escaped = !escaped && quote != "`" && next == "\\";
}
if (end || !(escaped || quote == "`"))
state.tokenize = tokenBase;
return "string";
};
}
function tokenComment(stream, state) {
var maybeEnd = false,
ch;
while (ch = stream.next()) {
if (ch == "/" && maybeEnd) {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return "comment";
}
function Context(indented, column, type, align, prev) {
this.indented = indented;
this.column = column;
this.type = type;
this.align = align;
this.prev = prev;
}
function pushContext(state, col, type) {
return state.context = new Context(state.indented, col, type, null, state.context);
}
function popContext(state) {
if (!state.context.prev) return;
var t = state.context.type;
if (t == ")" || t == "]" || t == "}")
state.indented = state.context.indented;
return state.context = state.context.prev;
}
// Interface
return {
startState: function (basecolumn) {
return {
tokenize: null,
context: new Context((basecolumn || 0) - indentUnit, 0, "top", false),
indented: 0,
startOfLine: true
};
},
token: function (stream, state) {
var ctx = state.context;
if (stream.sol()) {
if (ctx.align == null) ctx.align = false;
state.indented = stream.indentation();
state.startOfLine = true;
}
if (stream.eatSpace()) return null;
curPunc = null;
var style = (state.tokenize || tokenBase)(stream, state);
if (style == "comment") return style;
if (ctx.align == null) ctx.align = true;
if (curPunc == "{") pushContext(state, stream.column(), "}");
else if (curPunc == "[") pushContext(state, stream.column(), "]");
else if (curPunc == "(") pushContext(state, stream.column(), ")");
else if (curPunc == "}" && ctx.type == "}") popContext(state);
else if (curPunc == ctx.type) popContext(state);
state.startOfLine = false;
return style;
},
indent: function (state, textAfter) {
if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass;
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
if (firstChar == "#" || firstChar == ";") return 0;
if (stream.sol()) {
if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) {
state.context.type = "}";
return ctx.indented;
}
var closing = firstChar == ctx.type;
if (ctx.align) return ctx.column + (closing ? 0 : 1);
else return ctx.indented + (closing ? 0 : indentUnit);
}
},
electricChars: "{}):",
closeBrackets: "()[]{}''\"\"``",
fold: "brace",
blockCommentStart: "/*",
blockCommentEnd: "*/",
lineComment: "//"
};
});
CodeMirror.defineMIME("text/x-hcl", "hcl");
CodeMirror.modeInfo.push({
ext: [ "hcl " ],
mime: "text/x-hcl",
mode: "hcl",
name: "Terraform (HCL)"
});
});

View File

@ -2,7 +2,7 @@ import keyboardActionService 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";
interface ContextMenuOptions<T> { export interface ContextMenuOptions<T> {
x: number; x: number;
y: number; y: number;
orientation?: "left"; orientation?: "left";
@ -17,17 +17,30 @@ interface MenuSeparatorItem {
title: "----"; title: "----";
} }
export interface MenuItemBadge {
title: string;
className?: string;
}
export interface MenuCommandItem<T> { 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[];
templateNoteId?: string; templateNoteId?: string;
enabled?: boolean; enabled?: boolean;
handler?: MenuHandler<T>; handler?: MenuHandler<T>;
items?: MenuItem<T>[] | null; items?: MenuItem<T>[] | null;
shortcut?: string; shortcut?: string;
spellingSuggestion?: string; spellingSuggestion?: string;
checked?: boolean;
columns?: number;
} }
export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem; export type MenuItem<T> = MenuCommandItem<T> | MenuSeparatorItem;
@ -146,17 +159,32 @@ class ContextMenu {
} else { } else {
const $icon = $("<span>"); const $icon = $("<span>");
if ("uiIcon" in item && item.uiIcon) { if ("uiIcon" in item || "checked" in item) {
$icon.addClass(item.uiIcon); const icon = (item.checked ? "bx bx-check" : item.uiIcon);
if (icon) {
$icon.addClass(icon);
} else { } else {
$icon.append("&nbsp;"); $icon.append("&nbsp;");
} }
}
const $link = $("<span>") const $link = $("<span>")
.append($icon) .append($icon)
.append(" &nbsp; ") // some space between icon and text .append(" &nbsp; ") // some space between icon and text
.append(item.title); .append(item.title);
if ("badges" in item && item.badges) {
for (let badge of item.badges) {
const badgeElement = $(`<span class="badge">`).text(badge.title);
if (badge.className) {
badgeElement.addClass(badge.className);
}
$link.append(badgeElement);
}
}
if ("shortcut" in item && item.shortcut) { if ("shortcut" in item && item.shortcut) {
$link.append($("<kbd>").text(item.shortcut)); $link.append($("<kbd>").text(item.shortcut));
} }
@ -194,14 +222,15 @@ class ContextMenu {
return false; return false;
}); });
if (!this.isMobile) { $item.on("mouseup", (e) => {
$item.on("mouseup", (e) =>{ // Prevent submenu from failing to expand on mobile
if (!this.isMobile || !("items" in item && item.items)) {
e.stopPropagation(); e.stopPropagation();
// Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below. // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below.
this.hide(); this.hide();
return false; return false;
});
} }
});
if ("enabled" in item && item.enabled !== undefined && !item.enabled) { if ("enabled" in item && item.enabled !== undefined && !item.enabled) {
$item.addClass("disabled"); $item.addClass("disabled");
@ -212,6 +241,9 @@ class ContextMenu {
$link.addClass("dropdown-toggle"); $link.addClass("dropdown-toggle");
const $subMenu = $("<ul>").addClass("dropdown-menu"); const $subMenu = $("<ul>").addClass("dropdown-menu");
if (!this.isMobile && item.columns) {
$subMenu.css("column-count", item.columns);
}
this.addItems($subMenu, item.items); this.addItems($subMenu, item.items);

View File

@ -16,7 +16,8 @@ function getItems(): MenuItem<CommandNames>[] {
return [ return [
{ title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" }, { title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external" },
{ title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" }, { title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right" },
{ title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" } { title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open" },
{ title: t("link_context_menu.open_note_in_popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit" }
]; ];
} }
@ -40,6 +41,8 @@ function handleLinkContextMenuItem(command: string | undefined, notePath: string
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInNewWindow") { } else if (command === "openNoteInNewWindow") {
appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope }); appContext.triggerCommand("openInWindow", { notePath, hoistedNoteId, viewScope });
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
} }
} }

View File

@ -70,8 +70,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const items: (MenuItem<TreeCommandNames> | null)[] = [ const items: (MenuItem<TreeCommandNames> | null)[] = [
{ 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-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 },
isHoisted isHoisted
? null ? null
@ -92,7 +92,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "insertNoteAfter", command: "insertNoteAfter",
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,
columns: 2
}, },
{ {
@ -100,7 +101,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
command: "insertChildNote", command: "insertChildNote",
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
}, },
{ title: "----" }, { title: "----" },
@ -127,12 +129,6 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
enabled: isNotRoot && parentNotSearch && noSelectedNotes && 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 },
{
title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`,
command: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
},
{ title: "----" }, { title: "----" },
@ -186,6 +182,13 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ 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.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate", enabled: isNotRoot && !isHoisted },
{
title: `${t("tree-context-menu.duplicate")} <kbd data-command="duplicateSubtree">`,
command: "duplicateSubtree",
uiIcon: "bx bx-outline",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptionsOrHelp
},
{ {
title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`,
command: "deleteNotes", command: "deleteNotes",
@ -244,6 +247,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
} else if (command === "convertNoteToAttachment") { } else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) { if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return; return;

View File

@ -3,19 +3,21 @@ 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";
async function addLabel(noteId: string, name: string, value: string = "") { async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, { await server.put(`notes/${noteId}/attribute`, {
type: "label", type: "label",
name: name, name: name,
value: value value: value,
isInheritable
}); });
} }
async function setLabel(noteId: string, name: string, value: string = "") { export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
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
}); });
} }
@ -49,7 +51,7 @@ function removeOwnedLabelByName(note: FNote, labelName: string) {
* @param name the name of the attribute to set. * @param name the name of the attribute to set.
* @param value the value of the attribute to set. * @param value the value of the attribute to set.
*/ */
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) { 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 });

View File

@ -95,7 +95,15 @@ 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) {
@ -110,11 +118,13 @@ 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);

View File

@ -15,6 +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";
const ACTION_GROUPS = [ const ACTION_GROUPS = [
{ {
@ -89,6 +91,17 @@ function parseActions(note: FNote) {
.filter((action) => !!action); .filter((action) => !!action);
} }
export async function executeBulkActions(targetNoteIds: string[], actions: BulkAction[], includeDescendants = false) {
await server.post("bulk-action/execute", {
noteIds: targetNoteIds,
includeDescendants,
actions
});
await ws.waitForMaxKnownEntityChangeId();
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
}
export default { export default {
addAction, addAction,
parseActions, parseActions,

View File

@ -1,6 +1,6 @@
import ScriptContext from "./script_context.js"; import ScriptContext from "./script_context.js";
import server from "./server.js"; import server from "./server.js";
import toastService from "./toast.js"; import toastService, { showError } from "./toast.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont
} catch (e: any) { } catch (e: any) {
const note = await froca.getNote(bundle.noteId); const note = await froca.getNote(bundle.noteId);
toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`); const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`;
showError(message);
logError(message);
} }
} }

View File

@ -4,7 +4,7 @@ import froca from "./froca.js";
import linkService from "./link.js"; import linkService from "./link.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toast from "./toast.js"; import { throwError } from "./ws.js";
let clipboardBranchIds: string[] = []; let clipboardBranchIds: string[] = [];
let clipboardMode: string | null = null; let clipboardMode: string | null = null;
@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }
@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) {
// copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places
} else { } else {
toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); throwError(`Unrecognized clipboard mode=${clipboardMode}`);
} }
} }

View File

@ -118,8 +118,17 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) { async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>) {
const blob = await note.getBlob(); const blob = await note.getBlob();
let content = blob?.content || "";
if (note.mime === "application/json") {
try {
content = JSON.stringify(JSON.parse(content), null, 4);
} catch (e) {
// Ignore JSON parsing errors.
}
}
const $codeBlock = $("<code>"); const $codeBlock = $("<code>");
$codeBlock.text(blob?.content || ""); $codeBlock.text(content);
$renderedContent.append($("<pre>").append($codeBlock)); $renderedContent.append($("<pre>").append($codeBlock));
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime)); await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
} }
@ -301,7 +310,7 @@ function getRenderingType(entity: FNote | FAttachment) {
if (type === "file" && mime === "application/pdf") { if (type === "file" && mime === "application/pdf") {
type = "pdf"; type = "pdf";
} else if (type === "file" && mime && CODE_MIME_TYPES.has(mime)) { } else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
type = "code"; type = "code";
} else if (type === "file" && mime && mime.startsWith("audio/")) { } else if (type === "file" && mime && mime.startsWith("audio/")) {
type = "audio"; type = "audio";

View File

@ -1,13 +1,54 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0], config).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
export function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
async function info(message: string) { 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((res) => return new Promise<boolean>((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)

View File

@ -0,0 +1,29 @@
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
export function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
export function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}

View File

@ -245,6 +245,10 @@ class FrocaImpl implements Froca {
} }
async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> { async getNotes(noteIds: string[] | JQuery<string>, silentNotFoundError = false): Promise<FNote[]> {
if (noteIds.length === 0) {
return [];
}
noteIds = Array.from(new Set(noteIds)); // make unique noteIds = Array.from(new Set(noteIds)); // make unique
const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]); const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]);

View File

@ -49,6 +49,13 @@ function setupGlobs() {
const string = e?.reason?.message?.toLowerCase(); const string = e?.reason?.message?.toLowerCase();
let message = "Uncaught error: "; let message = "Uncaught error: ";
let errorObjectString;
try {
errorObjectString = JSON.stringify(e.reason)
} catch (error: any) {
errorObjectString = error.toString();
}
if (string?.includes("script error")) { if (string?.includes("script error")) {
message += "No details available"; message += "No details available";
@ -57,7 +64,7 @@ function setupGlobs() {
`Message: ${e.reason.message}`, `Message: ${e.reason.message}`,
`Line: ${e.reason.lineNumber}`, `Line: ${e.reason.lineNumber}`,
`Column: ${e.reason.columnNumber}`, `Column: ${e.reason.columnNumber}`,
`Error object: ${JSON.stringify(e.reason)}`, `Error object: ${errorObjectString}`,
`Stack: ${e.reason && e.reason.stack}` `Stack: ${e.reason && e.reason.stack}`
].join(", "); ].join(", ");
} }

View File

@ -1,6 +1,7 @@
import { LOCALES } from "@triliumnext/commons"; import { LOCALES } from "@triliumnext/commons";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import { describe, expect, it } from "vitest";
describe("i18n", () => { describe("i18n", () => {
it("translations are valid JSON", () => { it("translations are valid JSON", () => {

View File

@ -1,5 +1,5 @@
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toastService from "./toast.js"; import toastService, { showError } from "./toast.js";
function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) { function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
try { try {
@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery<HTMLElement>) {
if (success) { if (success) {
toastService.showMessage(t("image.copied-to-clipboard")); toastService.showMessage(t("image.copied-to-clipboard"));
} else { } else {
toastService.showAndLogError(t("image.cannot-copy")); const message = t("image.cannot-copy");
showError(message);
logError(message);
} }
} finally { } finally {
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();

View File

@ -231,6 +231,7 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
let ntxId: string | null = null; let ntxId: string | null = null;
let hoistedNoteId: string | null = null; let hoistedNoteId: string | null = null;
let searchString: string | null = null; let searchString: string | null = null;
let openInPopup = false;
if (paramString) { if (paramString) {
for (const pair of paramString.split("&")) { for (const pair of paramString.split("&")) {
@ -246,6 +247,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla searchString = value; // supports triggering search from URL, e.g. #?searchString=blabla
} else if (["viewMode", "attachmentId"].includes(name)) { } else if (["viewMode", "attachmentId"].includes(name)) {
(viewScope as any)[name] = value; (viewScope as any)[name] = value;
} else if (name === "popup") {
openInPopup = true;
} else { } else {
console.warn(`Unrecognized hash parameter '${name}'.`); console.warn(`Unrecognized hash parameter '${name}'.`);
} }
@ -266,7 +269,8 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
ntxId, ntxId,
hoistedNoteId, hoistedNoteId,
viewScope, viewScope,
searchString searchString,
openInPopup
}; };
} }
@ -277,13 +281,21 @@ function goToLink(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent) {
return goToLinkExt(evt, hrefLink, $link); return goToLinkExt(evt, hrefLink, $link);
} }
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) { /**
* Handles navigation to a link, which can be an internal note path (e.g., `#root/1234`) or an external URL (e.g., `https://example.com`).
*
* @param evt the event that triggered the link navigation, or `null` if the link was clicked programmatically. Used to determine if the link should be opened in a new tab/window, based on the button presses.
* @param hrefLink the link to navigate to, which can be a note path (e.g., `#root/1234`) or an external URL with any supported protocol (e.g., `https://example.com`).
* @param $link the jQuery element of the link that was clicked, used to determine if the link is an anchor link (e.g., `#fn1` or `#fnref1`) and to handle it accordingly.
* @returns `true` if the link was handled (i.e., the element was found and scrolled to), or a falsy value otherwise.
*/
function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, hrefLink: string | undefined, $link?: JQuery<HTMLElement> | null) {
if (hrefLink?.startsWith("data:")) { if (hrefLink?.startsWith("data:")) {
return true; return true;
} }
evt.preventDefault(); evt?.preventDefault();
evt.stopPropagation(); evt?.stopPropagation();
if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) { if (hrefLink && hrefLink.startsWith("#") && !hrefLink.startsWith("#root/") && $link) {
if (handleAnchor(hrefLink, $link)) { if (handleAnchor(hrefLink, $link)) {
@ -291,19 +303,22 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
} }
} }
const { notePath, viewScope } = parseNavigationStateFromUrl(hrefLink); const { notePath, viewScope, openInPopup } = parseNavigationStateFromUrl(hrefLink);
const ctrlKey = utils.isCtrlKey(evt); const ctrlKey = evt && utils.isCtrlKey(evt);
const shiftKey = evt.shiftKey; const shiftKey = evt?.shiftKey;
const isLeftClick = "which" in evt && evt.which === 1; const isLeftClick = !evt || ("which" in evt && evt.which === 1);
const isMiddleClick = "which" in evt && evt.which === 2; // Right click is handled separately.
const isMiddleClick = evt && "which" in evt && evt.which === 2;
const targetIsBlank = ($link?.attr("target") === "_blank"); const targetIsBlank = ($link?.attr("target") === "_blank");
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank; const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick || targetIsBlank;
const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey); const activate = (isLeftClick && ctrlKey && shiftKey) || (isMiddleClick && shiftKey);
const openInNewWindow = isLeftClick && evt.shiftKey && !ctrlKey; const openInNewWindow = isLeftClick && evt?.shiftKey && !ctrlKey;
if (notePath) { if (notePath) {
if (openInNewWindow) { if (isLeftClick && openInPopup) {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
} else if (openInNewWindow) {
appContext.triggerCommand("openInWindow", { notePath, viewScope }); appContext.triggerCommand("openInWindow", { notePath, viewScope });
} else if (openInNewTab) { } else if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath, { appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
@ -311,7 +326,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
viewScope viewScope
}); });
} else if (isLeftClick) { } else if (isLeftClick) {
const ntxId = $(evt.target as any) const ntxId = $(evt?.target as any)
.closest("[data-ntx-id]") .closest("[data-ntx-id]")
.attr("data-ntx-id"); .attr("data-ntx-id");
@ -379,6 +394,12 @@ function linkContextMenu(e: PointerEvent) {
return; return;
} }
if (utils.isCtrlKey(e) && e.button === 2) {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
e.preventDefault();
return;
}
e.preventDefault(); e.preventDefault();
linkContextMenuService.openContextMenu(notePath, e, viewScope, null); linkContextMenuService.openContextMenu(notePath, e, viewScope, null);

View File

@ -40,7 +40,10 @@ interface Options {
allowCreatingNotes?: boolean; allowCreatingNotes?: boolean;
allowJumpToSearchNotes?: boolean; allowJumpToSearchNotes?: boolean;
allowExternalLinks?: boolean; allowExternalLinks?: boolean;
/** If set, hides the right-side button corresponding to go to selected note. */
hideGoToSelectedNoteButton?: boolean; hideGoToSelectedNoteButton?: boolean;
/** If set, hides all right-side buttons in the autocomplete dropdown */
hideAllButtons?: boolean;
} }
async function autocompleteSourceForCKEditor(queryText: string) { async function autocompleteSourceForCKEditor(queryText: string) {
@ -190,9 +193,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right"); const $goToSelectedNoteButton = $("<a>").addClass("input-group-text go-to-selected-note-button bx bx-arrow-to-right");
if (!options.hideAllButtons) {
$el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton); $el.after($clearTextButton).after($showRecentNotesButton).after($fullTextSearchButton);
}
if (!options.hideGoToSelectedNoteButton) { if (!options.hideGoToSelectedNoteButton && !options.hideAllButtons) {
$el.after($goToSelectedNoteButton); $el.after($goToSelectedNoteButton);
} }
@ -289,13 +294,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
} }
if (suggestion.action === "create-note") { if (suggestion.action === "create-note") {
const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType();
if (!success) { if (!success) {
return; return;
} }
const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, {
const { note } = await noteCreateService.createNote(suggestion.parentNoteId, {
title: suggestion.noteTitle, title: suggestion.noteTitle,
activate: false, activate: false,
type: noteType, type: noteType,

View File

@ -11,7 +11,7 @@ import type FBranch from "../entities/fbranch.js";
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
interface CreateNoteOpts { export interface CreateNoteOpts {
isProtected?: boolean; isProtected?: boolean;
saveSelection?: boolean; saveSelection?: boolean;
title?: string | null; title?: string | null;
@ -116,7 +116,7 @@ async function chooseNoteType() {
} }
async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) {
const { success, noteType, templateNoteId } = await chooseNoteType(); const { success, noteType, templateNoteId, notePath } = await chooseNoteType();
if (!success) { if (!success) {
return; return;
@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN
options.type = noteType; options.type = noteType;
options.templateNoteId = templateNoteId; options.templateNoteId = templateNoteId;
return await createNote(parentNotePath, options); return await createNote(notePath || parentNotePath, options);
} }
/* If the first element is heading, parse it out and use it as a new heading. */ /* If the first element is heading, parse it out and use it as a new heading. */

View File

@ -1,38 +1,31 @@
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import BoardView from "../widgets/view_widgets/board_view/index.js";
import CalendarView from "../widgets/view_widgets/calendar_view.js"; import CalendarView from "../widgets/view_widgets/calendar_view.js";
import GeoView from "../widgets/view_widgets/geo_view/index.js";
import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js"; import ListOrGridView from "../widgets/view_widgets/list_or_grid_view.js";
import TableView from "../widgets/view_widgets/table_view/index.js";
import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js"; import type { ViewModeArgs } from "../widgets/view_widgets/view_mode.js";
import type ViewMode from "../widgets/view_widgets/view_mode.js"; import type ViewMode from "../widgets/view_widgets/view_mode.js";
export type ViewTypeOptions = "list" | "grid" | "calendar"; const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
export type ArgsWithoutNoteId = Omit<ViewModeArgs, "noteIds">;
export type ViewTypeOptions = typeof allViewTypes[number];
export default class NoteListRenderer { export default class NoteListRenderer {
private viewType: ViewTypeOptions; private viewType: ViewTypeOptions;
public viewMode: ViewMode | null; private args: ArgsWithoutNoteId;
public viewMode?: ViewMode<any>;
constructor($parent: JQuery<HTMLElement>, parentNote: FNote, noteIds: string[], showNotePath: boolean = false) { constructor(args: ArgsWithoutNoteId) {
this.viewType = this.#getViewType(parentNote); this.args = args;
const args: ViewModeArgs = { this.viewType = this.#getViewType(args.parentNote);
$parent,
parentNote,
noteIds,
showNotePath
};
if (this.viewType === "list" || this.viewType === "grid") {
this.viewMode = new ListOrGridView(this.viewType, args);
} else if (this.viewType === "calendar") {
this.viewMode = new CalendarView(args);
} else {
this.viewMode = null;
}
} }
#getViewType(parentNote: FNote): ViewTypeOptions { #getViewType(parentNote: FNote): ViewTypeOptions {
const viewType = parentNote.getLabelValue("viewType"); const viewType = parentNote.getLabelValue("viewType");
if (!["list", "grid", "calendar"].includes(viewType || "")) { if (!(allViewTypes as readonly string[]).includes(viewType || "")) {
// when not explicitly set, decide based on the note type // when not explicitly set, decide based on the note type
return parentNote.type === "search" ? "list" : "grid"; return parentNote.type === "search" ? "list" : "grid";
} else { } else {
@ -41,15 +34,38 @@ export default class NoteListRenderer {
} }
get isFullHeight() { get isFullHeight() {
return this.viewMode?.isFullHeight; switch (this.viewType) {
case "list":
case "grid":
return false;
default:
return true;
}
} }
async renderList() { async renderList() {
if (!this.viewMode) { const args = this.args;
return null; const viewMode = this.#buildViewMode(args);
this.viewMode = viewMode;
await viewMode.beforeRender();
return await viewMode.renderList();
} }
return await this.viewMode.renderList(); #buildViewMode(args: ViewModeArgs) {
switch (this.viewType) {
case "calendar":
return new CalendarView(args);
case "table":
return new TableView(args);
case "geoMap":
return new GeoView(args);
case "board":
return new BoardView(args);
case "list":
case "grid":
default:
return new ListOrGridView(this.viewType, args);
}
} }
} }

View File

@ -14,6 +14,7 @@ let dismissTimer: ReturnType<typeof setTimeout>;
function setupGlobalTooltip() { function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler); $(document).on("mouseenter", "a", mouseEnterHandler);
$(document).on("mouseenter", "[data-href]", mouseEnterHandler);
// close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen
$(document).on("click", (e) => { $(document).on("click", (e) => {
@ -167,7 +168,10 @@ async function renderTooltip(note: FNote | null) {
if (isContentEmpty) { if (isContentEmpty) {
classes.push("note-no-content"); classes.push("note-no-content");
} }
content = `<h5 class="${classes.join(" ")}"><a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a></h5>`; content = `\
<h5 class="${classes.join(" ")}">
<a href="#${note.noteId}" data-no-context-menu="true">${noteTitleWithPathAsSuffix.prop("outerHTML")}</a>
</h5>`;
} }
content = `${content}<div class="note-tooltip-attributes">${$renderedAttributes[0].outerHTML}</div>`; content = `${content}<div class="note-tooltip-attributes">${$renderedAttributes[0].outerHTML}</div>`;
@ -175,6 +179,7 @@ async function renderTooltip(note: FNote | null) {
content += $renderedContent[0].outerHTML; content += $renderedContent[0].outerHTML;
} }
content += `<a class="open-popup-button" title="${t("note_tooltip.quick-edit")}" href="#${note.noteId}?popup"><span class="bx bx-edit" /></a>`;
return content; return content;
} }

View File

@ -1,43 +1,236 @@
import server from "./server.js";
import froca from "./froca.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import type { MenuItem } from "../menus/context_menu.js"; import froca from "./froca.js";
import server from "./server.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge } from "../menus/context_menu.js";
import type { NoteType } from "../entities/fnote.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js";
export interface NoteTypeMapping {
type: NoteType;
mime?: string;
title: string;
icon?: string;
/** Indicates whether this type should be marked as a newly introduced feature. */
isNew?: boolean;
/** Indicates that this note type is part of a beta feature. */
isBeta?: boolean;
/** Indicates that this note type cannot be created by the user. */
reserved?: boolean;
/** Indicates that once a note of this type is created, its type can no longer be changed. */
static?: boolean;
}
export const NOTE_TYPES: NoteTypeMapping[] = [
// The suggested note type ordering method: insert the item into the corresponding group,
// then ensure the items within the group are ordered alphabetically.
// The default note type (always the first item)
{ type: "text", mime: "text/html", title: t("note_types.text"), icon: "bx-note" },
// Text notes group
{ type: "book", mime: "", title: t("note_types.book"), icon: "bx-book" },
// Graphic notes
{ type: "canvas", mime: "application/json", title: t("note_types.canvas"), icon: "bx-pen" },
{ type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), icon: "bx-selection" },
// Map notes
{ type: "mindMap", mime: "application/json", title: t("note_types.mind-map"), icon: "bx-sitemap" },
{ type: "noteMap", mime: "", title: t("note_types.note-map"), icon: "bxs-network-chart", static: true },
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
// Misc note types
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
// Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), icon: "bx-code" },
// Reserved types (cannot be created by the user)
{ type: "contentWidget", mime: "", title: t("note_types.widget"), reserved: true },
{ type: "doc", mime: "", title: t("note_types.doc"), reserved: true },
{ type: "file", title: t("note_types.file"), reserved: true },
{ type: "image", title: t("note_types.image"), reserved: true },
{ type: "launcher", mime: "", title: t("note_types.launcher"), reserved: true },
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), reserved: true }
];
/** The maximum age in days for a template to be marked with the "New" badge */
const NEW_TEMPLATE_MAX_AGE = 3;
/** The length of a day in milliseconds. */
const DAY_LENGTH = 1000 * 60 * 60 * 24;
/** The menu item badge used to mark new note types and templates */
const NEW_BADGE: MenuItemBadge = {
title: t("note_types.new-feature"),
className: "new-note-type-badge"
};
/** The menu item badge used to mark note types that are part of a beta feature */
const BETA_BADGE = {
title: t("note_types.beta-feature")
};
const SEPARATOR = { title: "----" };
const creationDateCache = new Map<string, Date>();
let rootCreationDate: Date | undefined;
async function getNoteTypeItems(command?: TreeCommandNames) { async function getNoteTypeItems(command?: TreeCommandNames) {
const items: MenuItem<TreeCommandNames>[] = [ const items: MenuItem<TreeCommandNames>[] = [
{ title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" }, ...getBlankNoteTypes(command),
{ title: t("note_types.code"), command, type: "code", uiIcon: "bx bx-code" }, ...await getBuiltInTemplates(t("note_types.collections"), command, true),
{ title: t("note_types.saved-search"), command, type: "search", uiIcon: "bx bx-file-find" }, ...await getBuiltInTemplates(null, command, false),
{ title: t("note_types.relation-map"), command, type: "relationMap", uiIcon: "bx bxs-network-chart" }, ...await getUserTemplates(command)
{ title: t("note_types.note-map"), command, type: "noteMap", uiIcon: "bx bxs-network-chart" },
{ title: t("note_types.render-note"), command, type: "render", uiIcon: "bx bx-extension" },
{ title: t("note_types.book"), command, type: "book", uiIcon: "bx bx-book" },
{ title: t("note_types.mermaid-diagram"), command, type: "mermaid", uiIcon: "bx bx-selection" },
{ title: t("note_types.canvas"), command, type: "canvas", uiIcon: "bx bx-pen" },
{ title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" },
{ title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" },
{ title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" },
]; ];
return items;
}
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
return NOTE_TYPES
.filter((nt) => !nt.reserved && nt.type !== "book")
.map((nt) => {
const menuItem: MenuCommandItem<TreeCommandNames> = {
title: nt.title,
command,
type: nt.type,
uiIcon: "bx " + nt.icon,
badges: []
}
if (nt.isNew) {
menuItem.badges?.push(NEW_BADGE);
}
if (nt.isBeta) {
menuItem.badges?.push(BETA_BADGE);
}
return menuItem;
});
}
async function getUserTemplates(command?: TreeCommandNames) {
const templateNoteIds = await server.get<string[]>("search-templates"); const templateNoteIds = await server.get<string[]>("search-templates");
const templateNotes = await froca.getNotes(templateNoteIds); const templateNotes = await froca.getNotes(templateNoteIds);
if (templateNotes.length === 0) {
return [];
}
if (templateNotes.length > 0) { const items: MenuItem<TreeCommandNames>[] = [
items.push({ title: "----" }); SEPARATOR
];
for (const templateNote of templateNotes) { for (const templateNote of templateNotes) {
items.push({ const item: MenuItem<TreeCommandNames> = {
title: templateNote.title, title: templateNote.title,
uiIcon: templateNote.getIcon(), uiIcon: templateNote.getIcon(),
command: command, command: command,
type: templateNote.type, type: templateNote.type,
templateNoteId: templateNote.noteId templateNoteId: templateNote.noteId
};
if (await isNewTemplate(templateNote.noteId)) {
item.badges = [NEW_BADGE];
}
items.push(item);
}
return items;
}
async function getBuiltInTemplates(title: string | null, command: TreeCommandNames | undefined, filterCollections: boolean) {
const templatesRoot = await froca.getNote("_templates");
if (!templatesRoot) {
console.warn("Unable to find template root.");
return [];
}
const childNotes = await templatesRoot.getChildNotes();
if (childNotes.length === 0) {
return [];
}
const items: MenuItem<TreeCommandNames>[] = [];
if (title) {
items.push({
title: title,
enabled: false,
uiIcon: "bx bx-empty"
}); });
} else {
items.push(SEPARATOR);
}
for (const templateNote of childNotes) {
if (templateNote.hasLabel("collection") !== filterCollections) {
continue;
}
const item: MenuItem<TreeCommandNames> = {
title: templateNote.title,
uiIcon: templateNote.getIcon(),
command: command,
type: templateNote.type,
templateNoteId: templateNote.noteId
};
if (await isNewTemplate(templateNote.noteId)) {
item.badges = [NEW_BADGE];
}
items.push(item);
}
return items;
}
async function isNewTemplate(templateNoteId) {
if (rootCreationDate === undefined) {
// Retrieve the root note creation date
try {
let rootNoteInfo: any = await server.get("notes/root");
if ("dateCreated" in rootNoteInfo) {
rootCreationDate = new Date(rootNoteInfo.dateCreated);
}
} catch (ex) {
console.error(ex);
} }
} }
return items; // Try to retrieve the template's creation date from the cache
let creationDate: Date | undefined = creationDateCache.get(templateNoteId);
if (creationDate === undefined) {
// The creation date isn't available in the cache, try to retrieve it from the server
try {
const noteInfo: any = await server.get("notes/" + templateNoteId);
if ("dateCreated" in noteInfo) {
creationDate = new Date(noteInfo.dateCreated);
creationDateCache.set(templateNoteId, creationDate);
}
} catch (ex) {
console.error(ex);
}
}
if (creationDate) {
if (rootCreationDate && creationDate.getTime() - rootCreationDate.getTime() < 30000) {
// Ignore templates created within 30 seconds after the root note is created.
// This is useful to prevent predefined templates from being marked
// as 'New' after setting up a new database.
return false;
}
// Determine the difference in days between now and the template's creation date
const age = (new Date().getTime() - creationDate.getTime()) / DAY_LENGTH;
// Return true if the template is at most NEW_TEMPLATE_MAX_AGE days old
return (age <= NEW_TEMPLATE_MAX_AGE);
} else {
return false;
}
} }
export default { export default {

View File

@ -1,4 +1,4 @@
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi"; type Multiplicity = "single" | "multi";
export interface DefinitionObject { export interface DefinitionObject {
@ -17,7 +17,7 @@ function parse(value: string) {
for (const token of tokens) { for (const token of tokens) {
if (token === "promoted") { if (token === "promoted") {
defObj.isPromoted = true; defObj.isPromoted = true;
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) { } else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
defObj.labelType = token as LabelType; defObj.labelType = token as LabelType;
} else if (["single", "multi"].includes(token)) { } else if (["single", "multi"].includes(token)) {
defObj.multiplicity = token as Multiplicity; defObj.multiplicity = token as Multiplicity;

View File

@ -1,4 +1,4 @@
import FrontendScriptApi, { type Entity } from "./frontend_script_api.js"; import type { Entity } from "./frontend_script_api.js";
import utils from "./utils.js"; import utils from "./utils.js";
import froca from "./froca.js"; import froca from "./froca.js";
@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn
throw new Error(`Could not find start note ${startNoteId}.`); throw new Error(`Could not find start note ${startNoteId}.`);
} }
const FrontendScriptApi = (await import("./frontend_script_api.js")).default;
return { return {
modules: modules, modules: modules,
notes: utils.toObject(allNotes, (note) => [note.noteId, note]), notes: utils.toObject(allNotes, (note) => [note.noteId, note]),

View File

@ -276,7 +276,8 @@ async function reportError(method: string, url: string, statusCode: number, resp
} else { } else {
const title = `${statusCode} ${method} ${url}`; const title = `${statusCode} ${method} ${url}`;
toastService.showErrorTitleAndMessage(title, messageStr); toastService.showErrorTitleAndMessage(title, messageStr);
toastService.throwError(`${title} - ${message}`); const { throwError } = await import("./ws.js");
throwError(`${title} - ${message}`);
} }
} }

View File

@ -51,6 +51,14 @@ export default class SpacedUpdate {
this.lastUpdated = Date.now(); this.lastUpdated = Date.now();
} }
/**
* Sets the update interval for the spaced update.
* @param interval The update interval in milliseconds.
*/
setUpdateInterval(interval: number) {
this.updateInterval = interval;
}
triggerUpdate() { triggerUpdate() {
if (!this.changed) { if (!this.changed) {
return; return;

View File

@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) {
}); });
} }
function showAndLogError(message: string, delay = 10000) { export function showError(message: string, delay = 10000) {
showError(message, delay);
ws.logError(message);
}
function showError(message: string, delay = 10000) {
console.log(utils.now(), "error: ", message); console.log(utils.now(), "error: ", message);
toast({ toast({
@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000)
}); });
} }
function throwError(message: string) {
ws.logError(message);
throw new Error(message);
}
export default { export default {
showMessage, showMessage,
showError, showError,
showErrorTitleAndMessage, showErrorTitleAndMessage,
showAndLogError,
throwError,
showPersistent, showPersistent,
closePersistent closePersistent
}; };

View File

@ -1,5 +1,4 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { Modal } from "bootstrap";
import type { ViewScope } from "./link.js"; import type { ViewScope } from "./link.js";
const SVG_MIME = "image/svg+xml"; const SVG_MIME = "image/svg+xml";
@ -275,71 +274,6 @@ function getMimeTypeClass(mime: string) {
return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`;
} }
function closeActiveDialog() {
if (glob.activeDialog) {
Modal.getOrCreateInstance(glob.activeDialog[0]).hide();
glob.activeDialog = null;
}
}
let $lastFocusedElement: JQuery<HTMLElement> | null;
// perhaps there should be saved focused element per tab?
function saveFocusedElement() {
$lastFocusedElement = $(":focus");
}
function focusSavedElement() {
if (!$lastFocusedElement) {
return;
}
if ($lastFocusedElement.hasClass("ck")) {
// must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607
// the bug manifests itself in resetting the cursor position to the first character - jumping above
const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance");
if (editor) {
editor.editing.view.focus();
} else {
console.log("Could not find CKEditor instance to focus last element");
}
} else {
$lastFocusedElement.focus();
}
$lastFocusedElement = null;
}
async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true) {
if (closeActDialog) {
closeActiveDialog();
glob.activeDialog = $dialog;
}
saveFocusedElement();
Modal.getOrCreateInstance($dialog[0]).show();
$dialog.on("hidden.bs.modal", () => {
const $autocompleteEl = $(".aa-input");
if ("autocomplete" in $autocompleteEl) {
$autocompleteEl.autocomplete("close");
}
if (!glob.activeDialog || glob.activeDialog === $dialog) {
focusSavedElement();
}
});
// TODO: Fix once keyboard_actions is ported.
// @ts-ignore
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;
}
function isHtmlEmpty(html: string) { function isHtmlEmpty(html: string) {
if (!html) { if (!html) {
return true; return true;
@ -825,10 +759,6 @@ export default {
setCookie, setCookie,
getNoteTypeClass, getNoteTypeClass,
getMimeTypeClass, getMimeTypeClass,
closeActiveDialog,
openDialog,
saveFocusedElement,
focusSavedElement,
isHtmlEmpty, isHtmlEmpty,
clearBrowserCache, clearBrowserCache,
copySelectionToClipboard, copySelectionToClipboard,

View File

@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastPingTs: number; let lastPingTs: number;
let frontendUpdateDataQueue: EntityChange[] = []; let frontendUpdateDataQueue: EntityChange[] = [];
function logError(message: string) { export function logError(message: string) {
console.error(utils.now(), message); // needs to be separate from .trace() console.error(utils.now(), message); // needs to be separate from .trace()
if (ws && ws.readyState === 1) { if (ws && ws.readyState === 1) {
@ -301,6 +301,12 @@ setTimeout(() => {
setInterval(sendPing, 1000); setInterval(sendPing, 1000);
}, 0); }, 0);
export function throwError(message: string) {
logError(message);
throw new Error(message);
}
export default { export default {
logError, logError,
subscribeToMessages, subscribeToMessages,

View File

@ -29,6 +29,14 @@ async function formatCodeBlocks() {
await formatCodeBlocks($("#content")); await formatCodeBlocks($("#content"));
} }
async function setupTextNote() {
formatCodeBlocks();
applyMath();
const setupMermaid = (await import("./share/mermaid.js")).default;
setupMermaid();
}
/** /**
* Fetch note with given ID from backend * Fetch note with given ID from backend
* *
@ -47,8 +55,11 @@ async function fetchNote(noteId: string | null = null) {
document.addEventListener( document.addEventListener(
"DOMContentLoaded", "DOMContentLoaded",
() => { () => {
formatCodeBlocks(); const noteType = determineNoteType();
applyMath();
if (noteType === "text") {
setupTextNote();
}
const toggleMenuButton = document.getElementById("toggleMenuButton"); const toggleMenuButton = document.getElementById("toggleMenuButton");
const layout = document.getElementById("layout"); const layout = document.getElementById("layout");
@ -60,6 +71,12 @@ document.addEventListener(
false false
); );
function determineNoteType() {
const bodyClass = document.body.className;
const match = bodyClass.match(/type-([^\s]+)/);
return match ? match[1] : null;
}
// workaround to prevent webpack from removing "fetchNote" as dead code: // workaround to prevent webpack from removing "fetchNote" as dead code:
// add fetchNote as property to the window object // add fetchNote as property to the window object
Object.defineProperty(window, "fetchNote", { Object.defineProperty(window, "fetchNote", {

View File

@ -0,0 +1,17 @@
import mermaid from "mermaid";
export default function setupMermaid() {
for (const codeBlock of document.querySelectorAll("#content pre code.language-mermaid")) {
const parentPre = codeBlock.parentElement;
if (!parentPre) {
continue;
}
const mermaidDiv = document.createElement("div");
mermaidDiv.classList.add("mermaid");
mermaidDiv.innerHTML = codeBlock.innerHTML;
parentPre.replaceWith(mermaidDiv);
}
mermaid.init();
}

View File

@ -81,8 +81,8 @@ body {
/* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */ /* -- Overrides the default colors used by the ckeditor5-image package. --------------------- */
--ck-color-image-caption-background: var(--main-background-color); --ck-content-color-image-caption-background: var(--main-background-color);
--ck-color-image-caption-text: var(--main-text-color); --ck-content-color-image-caption-text: var(--main-text-color);
/* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */ /* -- Overrides the default colors used by the ckeditor5-widget package. -------------------- */

View File

@ -25,6 +25,7 @@
--bs-body-font-weight: var(--main-font-weight) !important; --bs-body-font-weight: var(--main-font-weight) !important;
--bs-body-color: var(--main-text-color) !important; --bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important; --bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
} }
.table { .table {
@ -138,12 +139,6 @@ textarea,
color: var(--muted-text-color); color: var(--muted-text-color);
} }
/* Restore default apperance */
input[type="number"],
input[type="checkbox"] {
appearance: auto !important;
}
/* Add a gap between consecutive radios / check boxes */ /* Add a gap between consecutive radios / check boxes */
label.tn-radio + label.tn-radio, label.tn-radio + label.tn-radio,
label.tn-checkbox + label.tn-checkbox { label.tn-checkbox + label.tn-checkbox {
@ -191,6 +186,13 @@ samp {
font-family: var(--monospace-font-family) !important; font-family: var(--monospace-font-family) !important;
} }
.badge {
--bs-badge-color: var(--muted-text-color);
margin-left: 8px;
background: var(--accented-background-color);
}
.input-group-text { .input-group-text {
background-color: var(--accented-background-color) !important; background-color: var(--accented-background-color) !important;
color: var(--muted-text-color) !important; color: var(--muted-text-color) !important;
@ -319,7 +321,8 @@ button kbd {
} }
} }
.dropdown-menu { .dropdown-menu,
.tabulator-popup-container {
color: var(--menu-text-color) !important; color: var(--menu-text-color) !important;
font-size: inherit; font-size: inherit;
background-color: var(--menu-background-color) !important; background-color: var(--menu-background-color) !important;
@ -329,7 +332,13 @@ button kbd {
--bs-dropdown-link-active-bg: var(--active-item-background-color) !important; --bs-dropdown-link-active-bg: var(--active-item-background-color) !important;
} }
body.desktop .dropdown-menu { .dropdown-menu .dropdown-divider {
break-before: avoid;
break-after: avoid;
}
body.desktop .dropdown-menu,
body.desktop .tabulator-popup-container {
border: 1px solid var(--dropdown-border-color); border: 1px solid var(--dropdown-border-color);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity)); box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
animation: dropdown-menu-opening 100ms ease-in; animation: dropdown-menu-opening 100ms ease-in;
@ -372,7 +381,8 @@ body.desktop .dropdown-menu {
} }
.dropdown-menu a:hover:not(.disabled), .dropdown-menu a:hover:not(.disabled),
.dropdown-item:hover:not(.disabled, .dropdown-item-container) { .dropdown-item:hover:not(.disabled, .dropdown-item-container),
.tabulator-menu-item:hover {
color: var(--hover-item-text-color) !important; color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important; background-color: var(--hover-item-background-color) !important;
border-color: var(--hover-item-border-color) !important; border-color: var(--hover-item-border-color) !important;
@ -391,7 +401,7 @@ body.desktop .dropdown-menu {
} }
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item, body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
body.desktop #context-menu-container .dropdown-item > span { body #context-menu-container .dropdown-item > span {
display: flex; display: flex;
align-items: center; align-items: center;
} }
@ -439,10 +449,11 @@ body.desktop #context-menu-container .dropdown-item > span {
border-radius: 6px; border-radius: 6px;
overflow: hidden; overflow: hidden;
margin: 4px; margin: 4px;
font-size: var(--monospace-font-size);
} }
body .cm-editor { .cm-scroller {
font-size: var(--monospace-font-size); font-family: var(--monospace-font-family) !important;
} }
body .cm-editor .cm-gutters { body .cm-editor .cm-gutters {
@ -526,6 +537,7 @@ button.btn-sm {
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */ /* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
min-width: 0; min-width: 0;
padding: 0; padding: 0;
z-index: 1000;
} }
pre:not(.hljs) { pre:not(.hljs) {
@ -757,6 +769,14 @@ table.promoted-attributes-in-tooltip th {
font-size: small; font-size: small;
} }
.note-tooltip-content .open-popup-button {
position: absolute;
right: 15px;
bottom: 8px;
font-size: 1.2em;
color: inherit;
}
.note-tooltip-attributes { .note-tooltip-attributes {
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
@ -898,6 +918,13 @@ div[data-notify="container"] {
font-family: var(--monospace-font-family); font-family: var(--monospace-font-family);
} }
.ck-content {
--ck-content-font-family: var(--detail-font-family);
--ck-content-font-size: 1.1em;
--ck-content-font-color: var(--main-text-color);
--ck-content-line-height: var(--bs-body-line-height);
}
.ck-content .table table th { .ck-content .table table th {
background-color: var(--accented-background-color); background-color: var(--accented-background-color);
} }
@ -1184,12 +1211,14 @@ body.mobile .dropdown-submenu > .dropdown-menu {
} }
#context-menu-container, #context-menu-container,
#context-menu-container .dropdown-menu { #context-menu-container .dropdown-menu,
padding: 3px 0 0; .tabulator-popup-container {
padding: 3px 0;
z-index: 2000; z-index: 2000;
} }
#context-menu-container .dropdown-item { #context-menu-container .dropdown-item,
.tabulator-menu .tabulator-menu-item {
padding: 0 7px 0 10px; padding: 0 7px 0 10px;
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -1273,6 +1302,29 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
white-space: normal !important; white-space: normal !important;
} }
/* Slash commands */
.ck.ck-slash-command-button {
padding: 0.5em 1em !important;
}
.ck.ck-slash-command-button__text-part,
.ck.ck-template-form__text-part {
margin-left: 0.5em;
line-height: 1.2em !important;
}
.ck.ck-slash-command-button__text-part > span,
.ck.ck-template-form__text-part > span {
line-height: inherit !important;
}
.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description,
.ck.ck-template-form__text-part .ck-template-form__description {
display: block;
opacity: 0.8;
}
.area-expander { .area-expander {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@ -0,0 +1,199 @@
.tabulator {
--table-background-color: var(--main-background-color);
--col-header-background-color: var(--main-background-color);
--col-header-hover-background-color: var(--accented-background-color);
--col-header-text-color: var(--main-text-color);
--col-header-arrow-active-color: var(--main-text-color);
--col-header-arrow-inactive-color: var(--more-accented-background-color);
--col-header-separator-border: none;
--col-header-bottom-border: 2px solid var(--main-border-color);
--row-background-color: var(--main-background-color);
--row-alternate-background-color: var(--main-background-color);
--row-moving-background-color: var(--accented-background-color);
--row-text-color: var(--main-text-color);
--row-delimiter-color: var(--more-accented-background-color);
--cell-horiz-padding-size: 8px;
--cell-vert-padding-size: 8px;
--cell-editable-hover-outline-color: var(--main-border-color);
--cell-read-only-text-color: var(--muted-text-color);
--cell-editing-border-color: var(--main-border-color);
--cell-editing-border-width: 2px;
--cell-editing-background-color: var(--ck-color-selector-focused-cell-background);
--cell-editing-text-color: initial;
background: unset;
border: unset;
}
.tabulator .tabulator-tableholder .tabulator-table {
background: var(--table-background-color);
}
/* Column headers */
.tabulator div.tabulator-header {
border-bottom: var(--col-header-bottom-border);
background: var(--col-header-background-color);
color: var(--col-header-text-color);
}
.tabulator .tabulator-col-content {
padding: 8px 4px !important;
}
@media (hover: hover) and (pointer: fine) {
.tabulator .tabulator-header .tabulator-col.tabulator-sortable.tabulator-col-sorter-element:hover {
background-color: var(--col-header-hover-background-color);
}
}
.tabulator div.tabulator-header .tabulator-col.tabulator-moving {
border: none;
background: var(--col-header-hover-background-color);
}
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-bottom-color: var(--col-header-arrow-active-color);
border-top-color: var(--col-header-arrow-active-color);
}
.tabulator .tabulator-header .tabulator-col.tabulator-sortable[aria-sort="none"] .tabulator-col-content .tabulator-col-sorter .tabulator-arrow {
border-bottom-color: var(--col-header-arrow-inactive-color);
}
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
margin-left: var(--cell-editing-border-width);
}
.tabulator div.tabulator-header .tabulator-col,
.tabulator div.tabulator-header .tabulator-frozen.tabulator-frozen-left {
background: var(--col-header-background-color);
border-right: var(--col-header-separator-border);
}
/* Table body */
.tabulator-tableholder {
padding-top: 10px;
height: unset !important; /* Don't extend on the full height */
}
/* Rows */
.tabulator-row .tabulator-cell {
padding: var(--cell-vert-padding-size) var(--cell-horiz-padding-size);
}
.tabulator-row .tabulator-cell input {
padding-left: var(--cell-horiz-padding-size) !important;
padding-right: var(--cell-horiz-padding-size) !important;
}
.tabulator-row {
background: transparent;
border-top: none;
border-bottom: 1px solid var(--row-delimiter-color);
color: var(--row-text-color);
}
.tabulator-row.tabulator-row-odd {
background: var(--row-background-color);
}
.tabulator-row.tabulator-row-even {
background: var(--row-alternate-background-color);
}
.tabulator-row.tabulator-moving {
border-color: transparent;
background-color: var(--row-moving-background-color);
}
/* Cell */
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left {
margin-right: var(--cell-editing-border-width);
}
.tabulator-row .tabulator-cell.tabulator-frozen.tabulator-frozen-left,
.tabulator-row .tabulator-cell {
border-right-color: transparent;
}
.tabulator-row .tabulator-cell:not(.tabulator-editable) {
color: var(--cell-read-only-text-color);
}
.tabulator:not(.tabulator-editing) .tabulator-row .tabulator-cell.tabulator-editable:hover {
outline: 2px solid var(--cell-editable-hover-outline-color);
outline-offset: -1px;
}
.tabulator-row .tabulator-cell.tabulator-editing {
border-color: transparent;
}
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing {
outline: calc(var(--cell-editing-border-width) - 1px) solid var(--cell-editing-border-color);
border-color: var(--cell-editing-border-color);
background: var(--cell-editing-background-color);
}
.tabulator-row:not(.tabulator-moving) .tabulator-cell.tabulator-editing > * {
color: var(--cell-editing-text-color);
}
.tabulator .tree-collapse,
.tabulator .tree-expand {
color: var(--row-text-color);
}
/* Align items without children/expander to the ones with. */
.tabulator-cell[tabulator-field="title"] > span:first-child, /* 1st level */
.tabulator-cell[tabulator-field="title"] > div:first-child + span { /* sub-level */
padding-left: 21px;
}
/* Checkbox cells */
.tabulator .tabulator-cell:has(svg),
.tabulator .tabulator-cell:has(input[type="checkbox"]) {
padding-left: 8px;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.tabulator .tabulator-cell input[type="checkbox"] {
margin: 0;
}
.tabulator .tabulator-footer {
color: var(--main-text-color);
}
/* Context menus */
.tabulator-popup-container {
min-width: 10em;
border-radius: var(--bs-border-radius);
}
.tabulator-menu .tabulator-menu-item {
border: 1px solid transparent;
color: var(--menu-text-color);
font-size: 16px;
}
/* Footer */
:root .tabulator .tabulator-footer {
border-top: unset;
padding: 10px 0;
}

View File

@ -178,6 +178,9 @@
--alert-bar-background: #6b6b6b3b; --alert-bar-background: #6b6b6b3b;
--badge-background-color: #ffffff1a;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color); --promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #000000b3; --promoted-attribute-card-shadow-color: #000000b3;

View File

@ -171,6 +171,9 @@
--alert-bar-background: #32637b29; --alert-bar-background: #32637b29;
--badge-background-color: #00000011;
--badge-text-color: var(--muted-text-color);
--promoted-attribute-card-background-color: var(--card-background-color); --promoted-attribute-card-background-color: var(--card-background-color);
--promoted-attribute-card-shadow-color: #00000033; --promoted-attribute-card-shadow-color: #00000033;

View File

@ -4,6 +4,7 @@
@import url(./pages.css); @import url(./pages.css);
@import url(./ribbon.css); @import url(./ribbon.css);
@import url(./notes/text.css); @import url(./notes/text.css);
@import url(./notes/collections/table.css);
@font-face { @font-face {
font-family: "Inter"; font-family: "Inter";
@ -171,9 +172,19 @@ html body .dropdown-item[disabled] {
opacity: var(--menu-item-disabled-opacity); opacity: var(--menu-item-disabled-opacity);
} }
/* Badges */
:root .badge {
--bs-badge-color: var(--badge-text-color);
--bs-badge-font-weight: 500;
background: var(--badge-background-color);
text-transform: uppercase;
letter-spacing: .2pt;
}
/* Menu item icon */ /* Menu item icon */
.dropdown-item .bx { .dropdown-item .bx {
transform: translateY(var(--menu-item-icon-vert-offset)); translate: 0 var(--menu-item-icon-vert-offset);
color: var(--menu-item-icon-color) !important; color: var(--menu-item-icon-color) !important;
font-size: 1.1em; font-size: 1.1em;
} }

View File

@ -382,6 +382,10 @@ div.tn-tool-dialog {
/* DELETE NOTE PREVIEW DIALOG */ /* DELETE NOTE PREVIEW DIALOG */
.delete-notes-dialog .modal-dialog {
--bs-modal-width: fit-content;
}
.delete-notes-list .note-path { .delete-notes-list .note-path {
padding-left: 8px; padding-left: 8px;
} }
@ -396,3 +400,19 @@ div.tn-tool-dialog {
font-weight: normal; font-weight: normal;
white-space: nowrap; white-space: nowrap;
} }
/*
* NOTE TYPE CHOOSER DIALOG
*/
.note-type-chooser-dialog div.note-type-dropdown {
/* Disable the active item highlighting since there is no use for it here */
--active-item-text-color: initial;
--active-item-background-color: initial;
font-size: unset;
}
.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.bx {
margin-right: .25em;
}

View File

@ -267,7 +267,7 @@ input::selection,
} }
.input-group button:focus-visible, .input-group button:focus-visible,
.input-group a:focus-visible { .input-group a:focus-visible:not(.dropdown-item) {
box-shadow: unset; box-shadow: unset;
outline: transparent; outline: transparent;
border: transparent; border: transparent;
@ -349,7 +349,7 @@ select:hover,
select.form-select:hover, select.form-select:hover,
select.form-control:hover, select.form-control:hover,
.select-button.dropdown-toggle.btn:hover { .select-button.dropdown-toggle.btn:hover {
background: var(--input-hover-background) var(--dropdown-arrow); background: var(--input-hover-background) var(--dropdown-arrow,);
color: var(--input-hover-color); color: var(--input-hover-color);
} }

View File

@ -0,0 +1,13 @@
:root .tabulator {
--col-header-hover-background-color: var(--hover-item-background-color);
--col-header-arrow-active-color: var(--active-item-text-color);
--col-header-arrow-inactive-color: var(--main-border-color);
--row-moving-background-color: var(--more-accented-background-color);
--cell-editable-hover-outline-color: var(--input-focus-outline-color);
--cell-editing-border-color: var(--input-focus-outline-color);
--cell-editing-background-color: var(--input-background-color);
--cell-editing-text-color: var(--input-text-color);
}

View File

@ -201,6 +201,11 @@
color: var(--menu-item-icon-color); color: var(--menu-item-icon-color);
} }
/* Slash commands */
.ck.ck-slash-command-button__text-part .ck.ck-button__label {
font-weight: bold;
}
/* Separator */ /* Separator */
:root .ck .ck-list__separator { :root .ck .ck-list__separator {
margin: .5em 0; margin: .5em 0;

View File

@ -142,6 +142,12 @@ div.note-detail-empty {
border: unset; border: unset;
} }
/* NOTE ATTACHMENTS */
.attachment-list div.links-wrapper {
font-size: unset;
}
/* /*
* OPTIONS PAGES * OPTIONS PAGES
*/ */

View File

@ -46,6 +46,12 @@ div.promoted-attributes-container {
.image-properties > div:first-child > span > strong { .image-properties > div:first-child > span > strong {
opacity: 0.65; opacity: 0.65;
font-weight: 500; font-weight: 500;
vertical-align: top;
}
.note-info-widget-table td,
.file-properties-widget .file-table td {
vertical-align: top;
} }
.file-properties-widget { .file-properties-widget {

View File

@ -71,12 +71,13 @@ body.background-effects.platform-win32.layout-vertical #vertical-main-container
/* #endregion */ /* #endregion */
/* Matches when the left pane is collapsed */ /* Matches when the left pane is collapsed */
:has(.layout-vertical #left-pane.hidden-int) { #horizontal-main-container.left-pane-hidden {
--center-pane-border-radius: 0; --center-pane-border-radius: 0;
--tab-first-item-horiz-offset: 5px; --tab-first-item-horiz-offset: 5px;
} }
:has(#left-pane.hidden-int) #launcher-pane.vertical { /* Add a border to the vertical launch bar if collapsed. */
body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.vertical {
border-right: 2px solid var(--left-pane-collapsed-border-color); border-right: 2px solid var(--left-pane-collapsed-border-color);
} }
@ -354,7 +355,7 @@ body.layout-horizontal > .horizontal {
} }
.calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button { .calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button {
--select-arrow-svg: ""; /* Disable the dropdown arrow */ --select-arrow-svg: initial; /* Disable the dropdown arrow */
} }
@media (max-width: 992px) { @media (max-width: 992px) {
@ -1145,12 +1146,18 @@ body.mobile .note-title {
/* The "Change note icon" button */ /* The "Change note icon" button */
.note-icon-widget .note-icon { :root .note-icon-widget button.note-icon,
:root .note-icon-widget button.note-icon:hover {
border: none; border: none;
border-radius: 8px; border-radius: 8px;
} }
.note-icon-widget .note-icon:hover { /* Dropdown open */
:root .note-icon-widget button.note-icon.show {
background: var(--ck-editor-toolbar-dropdown-button-open-background);
}
:root .note-icon-widget button.note-icon:not(:disabled):hover {
background: var(--icon-button-hover-background); background: var(--icon-button-hover-background);
color: var(--icon-button-hover-color); color: var(--icon-button-hover-color);
} }
@ -1294,9 +1301,9 @@ div.promoted-attribute-cell .tn-checkbox {
height: 1cap; height: 1cap;
} }
/* The <div> containing the checkbox for a promoted boolean attribute */ /* Relocate the checkbox before the label */
div.promoted-attribute-cell div:has(input[type="checkbox"]) { div.promoted-attribute-cell.promoted-attribute-label-boolean > div:first-of-type {
order: -1; /* Relocate the checkbox before the label */ order: -1;
margin-right: 1.5em; margin-right: 1.5em;
} }
@ -1672,3 +1679,41 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
background: transparent; background: transparent;
transition: none; transition: none;
} }
/** Canvas **/
.excalidraw {
--border-radius-lg: 6px;
}
.excalidraw .Island {
backdrop-filter: var(--dropdown-backdrop-filter);
}
.excalidraw .Island.App-toolbar {
--island-bg-color: var(--floating-button-background-color);
--shadow-island: 1px 1px 1px var(--floating-button-shadow-color);
}
.excalidraw .dropdown-menu {
border: unset !important;
box-shadow: unset !important;
background-color: transparent !important;
--island-bg-color: var(--menu-background-color);
--shadow-island: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
--default-border-color: var(--bs-dropdown-divider-bg);
--button-hover-bg: var(--hover-item-background-color);
}
.excalidraw .dropdown-menu .dropdown-menu-container {
border-radius: var(--dropdown-border-radius);
}
.excalidraw .dropdown-menu .dropdown-menu-container > div:not([class]):not(:last-child) {
margin-left: calc(var(--padding) * var(--space-factor) * -1) !important;
margin-right: calc(var(--padding) * var(--space-factor) * -1) !important;
}
.excalidraw .dropdown-menu:before {
content: unset !important;
}

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "关于 TriliumNext Notes", "title": "关于 Trilium Notes",
"close": "关闭", "close": "关闭",
"homepage": "项目主页:", "homepage": "项目主页:",
"app_version": "应用版本:", "app_version": "应用版本:",
@ -754,7 +754,7 @@
"expand_all_children": "展开所有子项", "expand_all_children": "展开所有子项",
"collapse": "折叠", "collapse": "折叠",
"expand": "展开", "expand": "展开",
"book_properties": "书籍属性", "book_properties": "",
"invalid_view_type": "无效的查看类型 '{{type}}'", "invalid_view_type": "无效的查看类型 '{{type}}'",
"calendar": "日历" "calendar": "日历"
}, },
@ -1431,7 +1431,6 @@
"move-to": "移动到...", "move-to": "移动到...",
"paste-into": "粘贴到里面", "paste-into": "粘贴到里面",
"paste-after": "粘贴到后面", "paste-after": "粘贴到后面",
"duplicate-subtree": "复制子树",
"export": "导出", "export": "导出",
"import-into-note": "导入到笔记", "import-into-note": "导入到笔记",
"apply-bulk-actions": "应用批量操作", "apply-bulk-actions": "应用批量操作",
@ -1450,7 +1449,7 @@
"relation-map": "关系图", "relation-map": "关系图",
"note-map": "笔记地图", "note-map": "笔记地图",
"render-note": "渲染笔记", "render-note": "渲染笔记",
"book": "", "book": "",
"mermaid-diagram": "Mermaid 图", "mermaid-diagram": "Mermaid 图",
"canvas": "画布", "canvas": "画布",
"web-view": "网页视图", "web-view": "网页视图",

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "Über TriliumNext Notes", "title": "Über Trilium Notes",
"close": "Schließen", "close": "Schließen",
"homepage": "Startseite:", "homepage": "Startseite:",
"app_version": "App-Version:", "app_version": "App-Version:",
@ -639,7 +639,7 @@
"reload_frontend": "Frontend neu laden", "reload_frontend": "Frontend neu laden",
"show_hidden_subtree": "Versteckten Teilbaum anzeigen", "show_hidden_subtree": "Versteckten Teilbaum anzeigen",
"show_help": "Hilfe anzeigen", "show_help": "Hilfe anzeigen",
"about": "Über TriliumNext Notes", "about": "Über Trilium Notes",
"logout": "Abmelden", "logout": "Abmelden",
"show-cheatsheet": "Cheatsheet anzeigen", "show-cheatsheet": "Cheatsheet anzeigen",
"toggle-zen-mode": "Zen Modus" "toggle-zen-mode": "Zen Modus"
@ -750,7 +750,7 @@
"expand_all_children": "Unternotizen ausklappen", "expand_all_children": "Unternotizen ausklappen",
"collapse": "Einklappen", "collapse": "Einklappen",
"expand": "Ausklappen", "expand": "Ausklappen",
"book_properties": "Bucheigenschaften", "book_properties": "",
"invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“", "invalid_view_type": "Ungültiger Ansichtstyp „{{type}}“",
"calendar": "Kalender" "calendar": "Kalender"
}, },
@ -1384,7 +1384,7 @@
"move-to": "Verschieben nach...", "move-to": "Verschieben nach...",
"paste-into": "Als Unternotiz einfügen", "paste-into": "Als Unternotiz einfügen",
"paste-after": "Danach einfügen", "paste-after": "Danach einfügen",
"duplicate-subtree": "Notizbaum duplizieren", "duplicate": "Duplizieren",
"export": "Exportieren", "export": "Exportieren",
"import-into-note": "In Notiz importieren", "import-into-note": "In Notiz importieren",
"apply-bulk-actions": "Massenaktionen ausführen", "apply-bulk-actions": "Massenaktionen ausführen",
@ -1403,7 +1403,7 @@
"relation-map": "Beziehungskarte", "relation-map": "Beziehungskarte",
"note-map": "Notizkarte", "note-map": "Notizkarte",
"render-note": "Render Notiz", "render-note": "Render Notiz",
"book": "Buch", "book": "",
"mermaid-diagram": "Mermaid Diagram", "mermaid-diagram": "Mermaid Diagram",
"canvas": "Canvas", "canvas": "Canvas",
"web-view": "Webansicht", "web-view": "Webansicht",

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "About TriliumNext Notes", "title": "About Trilium Notes",
"close": "Close", "close": "Close",
"homepage": "Homepage:", "homepage": "Homepage:",
"app_version": "App version:", "app_version": "App version:",
@ -233,6 +233,8 @@
"move_success_message": "Selected notes have been moved into " "move_success_message": "Selected notes have been moved into "
}, },
"note_type_chooser": { "note_type_chooser": {
"change_path_prompt": "Change where to create the new note:",
"search_placeholder": "search path by name (default if empty)",
"modal_title": "Choose note type", "modal_title": "Choose note type",
"close": "Close", "close": "Close",
"modal_body": "Choose note type / template of the new note:", "modal_body": "Choose note type / template of the new note:",
@ -441,7 +443,8 @@
"other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"", "other_notes_with_name": "Other notes with {{attributeType}} name \"{{attributeName}}\"",
"and_more": "... and {{count}} more.", "and_more": "... and {{count}} more.",
"print_landscape": "When exporting to PDF, changes the orientation of the page to landscape instead of portrait.", "print_landscape": "When exporting to PDF, changes the orientation of the page to landscape instead of portrait.",
"print_page_size": "When exporting to PDF, changes the size of the page. Supported values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>." "print_page_size": "When exporting to PDF, changes the size of the page. Supported values: <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Color"
}, },
"attribute_editor": { "attribute_editor": {
"help_text_body1": "To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code>", "help_text_body1": "To add label, just type e.g. <code>#rock</code> or if you want to add also value then e.g. <code>#year = 2020</code>",
@ -641,7 +644,7 @@
"reload_frontend": "Reload Frontend", "reload_frontend": "Reload Frontend",
"show_hidden_subtree": "Show Hidden Subtree", "show_hidden_subtree": "Show Hidden Subtree",
"show_help": "Show Help", "show_help": "Show Help",
"about": "About TriliumNext Notes", "about": "About Trilium Notes",
"logout": "Logout", "logout": "Logout",
"show-cheatsheet": "Show Cheatsheet", "show-cheatsheet": "Show Cheatsheet",
"toggle-zen-mode": "Zen Mode" "toggle-zen-mode": "Zen Mode"
@ -756,9 +759,12 @@
"expand_all_children": "Expand all children", "expand_all_children": "Expand all children",
"collapse": "Collapse", "collapse": "Collapse",
"expand": "Expand", "expand": "Expand",
"book_properties": "Book Properties", "book_properties": "Collection Properties",
"invalid_view_type": "Invalid view type '{{type}}'", "invalid_view_type": "Invalid view type '{{type}}'",
"calendar": "Calendar" "calendar": "Calendar",
"table": "Table",
"geo-map": "Geo Map",
"board": "Board"
}, },
"edited_notes": { "edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...", "no_edited_notes_found": "No edited notes on this day yet...",
@ -835,7 +841,8 @@
"unknown_label_type": "Unknown label type '{{type}}'", "unknown_label_type": "Unknown label type '{{type}}'",
"unknown_attribute_type": "Unknown attribute type '{{type}}'", "unknown_attribute_type": "Unknown attribute type '{{type}}'",
"add_new_attribute": "Add new attribute", "add_new_attribute": "Add new attribute",
"remove_this_attribute": "Remove this attribute" "remove_this_attribute": "Remove this attribute",
"remove_color": "Remove the color label"
}, },
"script_executor": { "script_executor": {
"query": "Query", "query": "Query",
@ -958,7 +965,7 @@
"no_attachments": "This note has no attachments." "no_attachments": "This note has no attachments."
}, },
"book": { "book": {
"no_children_help": "This note of type Book doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details." "no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details."
}, },
"editable_code": { "editable_code": {
"placeholder": "Type the content of your code note here..." "placeholder": "Type the content of your code note here..."
@ -1021,7 +1028,7 @@
"title": "Consistency Checks", "title": "Consistency Checks",
"find_and_fix_button": "Find and fix consistency issues", "find_and_fix_button": "Find and fix consistency issues",
"finding_and_fixing_message": "Finding and fixing consistency issues...", "finding_and_fixing_message": "Finding and fixing consistency issues...",
"issues_fixed_message": "Consistency issues should be fixed." "issues_fixed_message": "Any consistency issue which may have been found is now fixed."
}, },
"database_anonymization": { "database_anonymization": {
"title": "Database Anonymization", "title": "Database Anonymization",
@ -1195,7 +1202,6 @@
"restore_provider": "Restore provider to search", "restore_provider": "Restore provider to search",
"similarity_threshold": "Similarity Threshold", "similarity_threshold": "Similarity Threshold",
"similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries", "similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries",
"reprocess_index": "Rebuild Search Index", "reprocess_index": "Rebuild Search Index",
"reprocessing_index": "Rebuilding...", "reprocessing_index": "Rebuilding...",
"reprocess_index_started": "Search index optimization started in the background", "reprocess_index_started": "Search index optimization started in the background",
@ -1592,12 +1598,13 @@
"move-to": "Move to...", "move-to": "Move to...",
"paste-into": "Paste into", "paste-into": "Paste into",
"paste-after": "Paste after", "paste-after": "Paste after",
"duplicate-subtree": "Duplicate subtree", "duplicate": "Duplicate",
"export": "Export", "export": "Export",
"import-into-note": "Import into note", "import-into-note": "Import into note",
"apply-bulk-actions": "Apply bulk actions", "apply-bulk-actions": "Apply bulk actions",
"converted-to-attachments": "{{count}} notes have been converted to attachments.", "converted-to-attachments": "{{count}} notes have been converted to attachments.",
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?" "convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?",
"open-in-popup": "Quick edit"
}, },
"shared_info": { "shared_info": {
"shared_publicly": "This note is shared publicly on", "shared_publicly": "This note is shared publicly on",
@ -1611,7 +1618,7 @@
"relation-map": "Relation Map", "relation-map": "Relation Map",
"note-map": "Note Map", "note-map": "Note Map",
"render-note": "Render Note", "render-note": "Render Note",
"book": "Book", "book": "Collection",
"mermaid-diagram": "Mermaid Diagram", "mermaid-diagram": "Mermaid Diagram",
"canvas": "Canvas", "canvas": "Canvas",
"web-view": "Web View", "web-view": "Web View",
@ -1625,7 +1632,9 @@
"geo-map": "Geo Map", "geo-map": "Geo Map",
"beta-feature": "Beta", "beta-feature": "Beta",
"ai-chat": "AI Chat", "ai-chat": "AI Chat",
"task-list": "Task List" "task-list": "Task List",
"new-feature": "New",
"collections": "Collections"
}, },
"protect_note": { "protect_note": {
"toggle-on": "Protect the note", "toggle-on": "Protect the note",
@ -1827,7 +1836,8 @@
"link_context_menu": { "link_context_menu": {
"open_note_in_new_tab": "Open note in a new tab", "open_note_in_new_tab": "Open note in a new tab",
"open_note_in_new_split": "Open note in a new split", "open_note_in_new_split": "Open note in a new split",
"open_note_in_new_window": "Open note in a new window" "open_note_in_new_window": "Open note in a new window",
"open_note_in_popup": "Quick edit"
}, },
"electron_integration": { "electron_integration": {
"desktop-application": "Desktop Application", "desktop-application": "Desktop Application",
@ -1847,7 +1857,8 @@
"full-text-search": "Full text search" "full-text-search": "Full text search"
}, },
"note_tooltip": { "note_tooltip": {
"note-has-been-deleted": "Note has been deleted." "note-has-been-deleted": "Note has been deleted.",
"quick-edit": "Quick edit"
}, },
"geo-map": { "geo-map": {
"create-child-note-title": "Create a new child note and add it to the map", "create-child-note-title": "Create a new child note and add it to the map",
@ -1856,7 +1867,8 @@
}, },
"geo-map-context": { "geo-map-context": {
"open-location": "Open location", "open-location": "Open location",
"remove-from-map": "Remove from map" "remove-from-map": "Remove from map",
"add-note": "Add a marker at this location"
}, },
"help-button": { "help-button": {
"title": "Open the relevant help page" "title": "Open the relevant help page"
@ -1918,5 +1930,62 @@
"title": "Appearance", "title": "Appearance",
"word_wrapping": "Word wrapping", "word_wrapping": "Word wrapping",
"color-scheme": "Color scheme" "color-scheme": "Color scheme"
},
"cpu_arch_warning": {
"title": "Please download the ARM64 version",
"message_macos": "TriliumNext is currently running under Rosetta 2 translation, which means you're using the Intel (x64) version on Apple Silicon Mac. This will significantly impact performance and battery life.",
"message_windows": "TriliumNext is currently running emulation, which means you're using the Intel (x64) version on a Windows on ARM device. This will significantly impact performance and battery life.",
"recommendation": "For the best experience, please download the native ARM64 version of TriliumNext from our releases page.",
"download_link": "Download Native Version",
"continue_anyway": "Continue Anyway",
"dont_show_again": "Don't show this warning again"
},
"editorfeatures": {
"title": "Features",
"emoji_completion_enabled": "Enable Emoji auto-completion",
"note_completion_enabled": "Enable note auto-completion"
},
"table_view": {
"new-row": "New row",
"new-column": "New column",
"sort-column-by": "Sort by \"{{title}}\"",
"sort-column-ascending": "Ascending",
"sort-column-descending": "Descending",
"sort-column-clear": "Clear sorting",
"hide-column": "Hide column \"{{title}}\"",
"show-hide-columns": "Show/hide columns",
"row-insert-above": "Insert row above",
"row-insert-below": "Insert row below",
"row-insert-child": "Insert child note",
"add-column-to-the-left": "Add column to the left",
"add-column-to-the-right": "Add column to the right",
"edit-column": "Edit column",
"delete_column_confirmation": "Are you sure you want to delete this column? The corresponding attribute will be removed from all notes.",
"delete-column": "Delete column",
"new-column-label": "Label",
"new-column-relation": "Relation"
},
"book_properties_config": {
"hide-weekends": "Hide weekends",
"display-week-numbers": "Display week numbers",
"map-style": "Map style:",
"max-nesting-depth": "Max nesting depth:",
"raster": "Raster",
"vector_light": "Vector (Light)",
"vector_dark": "Vector (Dark)",
"show-scale": "Show scale"
},
"table_context_menu": {
"delete_row": "Delete row"
},
"board_view": {
"delete-note": "Delete Note",
"move-to": "Move to",
"insert-above": "Insert above",
"insert-below": "Insert below",
"delete-column": "Delete column",
"delete-column-confirmation": "Are you sure you want to delete this column? The corresponding attribute will be deleted in the notes under this column as well.",
"new-item": "New item",
"add-column": "Add Column"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "Acerca de TriliumNext Notes", "title": "Acerca de Trilium Notes",
"close": "Cerrar", "close": "Cerrar",
"homepage": "Página principal:", "homepage": "Página principal:",
"app_version": "Versión de la aplicación:", "app_version": "Versión de la aplicación:",
@ -16,7 +16,7 @@
"message": "Ha ocurrido un error crítico que previene que el cliente de la aplicación inicie:\n\n{{message}}\n\nMuy probablemente es causado por un script que falla de forma inesperada. Intente iniciar la aplicación en modo seguro y atienda el error." "message": "Ha ocurrido un error crítico que previene que el cliente de la aplicación inicie:\n\n{{message}}\n\nMuy probablemente es causado por un script que falla de forma inesperada. Intente iniciar la aplicación en modo seguro y atienda el error."
}, },
"widget-error": { "widget-error": {
"title": "No se pudo inicializar un widget", "title": "Hubo un fallo al inicializar un widget",
"message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}", "message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}",
"message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}" "message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}"
}, },
@ -127,6 +127,7 @@
"collapseSubTree": "colapsar subárbol", "collapseSubTree": "colapsar subárbol",
"tabShortcuts": "Atajos de pestañas", "tabShortcuts": "Atajos de pestañas",
"newTabNoteLink": "<kbd>CTRL+clic</kbd> - (o clic central del mouse) en el enlace de la nota abre la nota en una nueva pestaña", "newTabNoteLink": "<kbd>CTRL+clic</kbd> - (o clic central del mouse) en el enlace de la nota abre la nota en una nueva pestaña",
"newTabWithActivationNoteLink": "<kbd>Ctrl+Shift+clic</kbd> - (o <kbd>Shift+clic de rueda de ratón</kbd>) en el enlace de la nota abre y activa la nota en una nueva pestaña",
"onlyInDesktop": "Solo en escritorio (compilación con Electron)", "onlyInDesktop": "Solo en escritorio (compilación con Electron)",
"openEmptyTab": "abrir pestaña vacía", "openEmptyTab": "abrir pestaña vacía",
"closeActiveTab": "cerrar pestaña activa", "closeActiveTab": "cerrar pestaña activa",
@ -232,6 +233,8 @@
"move_success_message": "Las notas seleccionadas se han movido a " "move_success_message": "Las notas seleccionadas se han movido a "
}, },
"note_type_chooser": { "note_type_chooser": {
"change_path_prompt": "Cambiar donde se creará la nueva nota:",
"search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)",
"modal_title": "Elija el tipo de nota", "modal_title": "Elija el tipo de nota",
"close": "Cerrar", "close": "Cerrar",
"modal_body": "Elija el tipo de nota/plantilla de la nueva nota:", "modal_body": "Elija el tipo de nota/plantilla de la nueva nota:",
@ -274,9 +277,9 @@
"revision_last_edited": "Esta revisión se editó por última vez en {{date}}", "revision_last_edited": "Esta revisión se editó por última vez en {{date}}",
"confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?", "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?",
"no_revisions": "Aún no hay revisiones para esta nota...", "no_revisions": "Aún no hay revisiones para esta nota...",
"restore_button": "", "restore_button": "Restaurar",
"confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.", "confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.",
"delete_button": "", "delete_button": "Eliminar",
"confirm_delete": "¿Quieres eliminar esta revisión?", "confirm_delete": "¿Quieres eliminar esta revisión?",
"revisions_deleted": "Se han eliminado las revisiones de nota.", "revisions_deleted": "Se han eliminado las revisiones de nota.",
"revision_restored": "Se ha restaurado la revisión de nota.", "revision_restored": "Se ha restaurado la revisión de nota.",
@ -588,6 +591,7 @@
"sat": "Sáb", "sat": "Sáb",
"sun": "Dom", "sun": "Dom",
"cannot_find_day_note": "No se puede encontrar la nota del día", "cannot_find_day_note": "No se puede encontrar la nota del día",
"cannot_find_week_note": "No se puede encontrar la nota de la semana",
"january": "Enero", "january": "Enero",
"febuary": "Febrero", "febuary": "Febrero",
"march": "Marzo", "march": "Marzo",
@ -639,7 +643,7 @@
"reload_frontend": "Recargar interfaz", "reload_frontend": "Recargar interfaz",
"show_hidden_subtree": "Mostrar subárbol oculto", "show_hidden_subtree": "Mostrar subárbol oculto",
"show_help": "Mostrar ayuda", "show_help": "Mostrar ayuda",
"about": "Acerca de TriliumNext Notes", "about": "Acerca de Trilium Notes",
"logout": "Cerrar sesión", "logout": "Cerrar sesión",
"show-cheatsheet": "Mostrar hoja de trucos", "show-cheatsheet": "Mostrar hoja de trucos",
"toggle-zen-mode": "Modo Zen" "toggle-zen-mode": "Modo Zen"
@ -754,7 +758,7 @@
"expand_all_children": "Ampliar todas las subnotas", "expand_all_children": "Ampliar todas las subnotas",
"collapse": "Colapsar", "collapse": "Colapsar",
"expand": "Expandir", "expand": "Expandir",
"book_properties": "Propiedades del libro", "book_properties": "",
"invalid_view_type": "Tipo de vista inválida '{{type}}'", "invalid_view_type": "Tipo de vista inválida '{{type}}'",
"calendar": "Calendario" "calendar": "Calendario"
}, },
@ -1121,6 +1125,148 @@
"layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)", "layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo." "layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo."
}, },
"ai_llm": {
"not_started": "No iniciado",
"title": "IA y ajustes de embeddings",
"processed_notes": "Notas procesadas",
"total_notes": "Notas totales",
"progress": "Progreso",
"queued_notes": "Notas en fila",
"failed_notes": "Notas fallidas",
"last_processed": "Última procesada",
"refresh_stats": "Recargar estadísticas",
"enable_ai_features": "Habilitar características IA/LLM",
"enable_ai_description": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM",
"openai_tab": "OpenAI",
"anthropic_tab": "Anthropic",
"voyage_tab": "Voyage AI",
"ollama_tab": "Ollama",
"enable_ai": "Habilitar características IA/LLM",
"enable_ai_desc": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM",
"provider_configuration": "Configuración de proveedor de IA",
"provider_precedence": "Precedencia de proveedor",
"provider_precedence_description": "Lista de proveedores en orden de precedencia separada por comas (p.e., 'openai,anthropic,ollama')",
"temperature": "Temperatura",
"temperature_description": "Controla la aleatoriedad de las respuestas (0 = determinista, 2 = aleatoriedad máxima)",
"system_prompt": "Mensaje de sistema",
"system_prompt_description": "Mensaje de sistema predeterminado utilizado para todas las interacciones de IA",
"openai_configuration": "Configuración de OpenAI",
"openai_settings": "Ajustes de OpenAI",
"api_key": "Clave API",
"url": "URL base",
"model": "Modelo",
"openai_api_key_description": "Tu clave API de OpenAI para acceder a sus servicios de IA",
"anthropic_api_key_description": "Tu clave API de Anthropic para acceder a los modelos Claude",
"default_model": "Modelo por defecto",
"openai_model_description": "Ejemplos: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
"base_url": "URL base",
"openai_url_description": "Por defecto: https://api.openai.com/v1",
"anthropic_settings": "Ajustes de Anthropic",
"anthropic_url_description": "URL base para la API de Anthropic (por defecto: https://api.anthropic.com)",
"anthropic_model_description": "Modelos Claude de Anthropic para el completado de chat",
"voyage_settings": "Ajustes de Voyage AI",
"ollama_settings": "Ajustes de Ollama",
"ollama_url_description": "URL para la API de Ollama (por defecto: http://localhost:11434)",
"ollama_model_description": "Modelo de Ollama a usar para el completado de chat",
"anthropic_configuration": "Configuración de Anthropic",
"voyage_configuration": "Configuración de Voyage AI",
"voyage_url_description": "Por defecto: https://api.voyageai.com/v1",
"ollama_configuration": "Configuración de Ollama",
"enable_ollama": "Habilitar Ollama",
"enable_ollama_description": "Habilitar Ollama para uso de modelo de IA local",
"ollama_url": "URL de Ollama",
"ollama_model": "Modelo de Ollama",
"refresh_models": "Refrescar modelos",
"refreshing_models": "Refrescando...",
"enable_automatic_indexing": "Habilitar indexado automático",
"rebuild_index": "Recrear índice",
"rebuild_index_error": "Error al comenzar la reconstrucción del índice. Consulte los registros para más detalles.",
"note_title": "Título de nota",
"error": "Error",
"last_attempt": "Último intento",
"actions": "Acciones",
"retry": "Reintentar",
"partial": "{{ percentage }}% completado",
"retry_queued": "Nota en la cola para reintento",
"retry_failed": "Hubo un fallo al poner en la cola a la nota para reintento",
"max_notes_per_llm_query": "Máximo de notas por consulta",
"max_notes_per_llm_query_description": "Número máximo de notas similares a incluir en el contexto IA",
"active_providers": "Proveedores activos",
"disabled_providers": "Proveedores deshabilitados",
"remove_provider": "Eliminar proveedor de la búsqueda",
"restore_provider": "Restaurar proveedor a la búsqueda",
"similarity_threshold": "Bias de similaridad",
"similarity_threshold_description": "Puntuación de similaridad mínima (0-1) para incluir notas en el contexto para consultas LLM",
"reprocess_index": "Reconstruir el índice de búsqueda",
"reprocessing_index": "Reconstruyendo...",
"reprocess_index_started": "La optimización de índice de búsqueda comenzó en segundo plano",
"reprocess_index_error": "Error al reconstruir el índice de búsqueda",
"index_rebuild_progress": "Progreso de reconstrucción de índice",
"index_rebuilding": "Optimizando índice ({{percentage}}%)",
"index_rebuild_complete": "Optimización de índice completa",
"index_rebuild_status_error": "Error al comprobar el estado de reconstrucción del índice",
"never": "Nunca",
"processing": "Procesando ({{percentage}}%)",
"incomplete": "Incompleto ({{percentage}}%)",
"complete": "Completo (100%)",
"refreshing": "Refrescando...",
"auto_refresh_notice": "Refrescar automáticamente cada {{seconds}} segundos",
"note_queued_for_retry": "Nota en la cola para reintento",
"failed_to_retry_note": "Hubo un fallo al reintentar nota",
"all_notes_queued_for_retry": "Todas las notas con fallo agregadas a la cola para reintento",
"failed_to_retry_all": "Hubo un fallo al reintentar notas",
"ai_settings": "Ajustes de IA",
"api_key_tooltip": "Clave API para acceder al servicio",
"empty_key_warning": {
"anthropic": "La clave API de Anthropic está vacía. Por favor, ingrese una clave API válida.",
"openai": "La clave API de OpenAI está vacía. Por favor, ingrese una clave API válida.",
"voyage": "La clave API de Voyage está vacía. Por favor, ingrese una clave API válida.",
"ollama": "La clave API de Ollama está vacía. Por favor, ingrese una clave API válida."
},
"agent": {
"processing": "Procesando...",
"thinking": "Pensando...",
"loading": "Cargando...",
"generating": "Generando..."
},
"name": "IA",
"openai": "OpenAI",
"use_enhanced_context": "Utilizar contexto mejorado",
"enhanced_context_description": "Provee a la IA con más contexto de la nota y sus notas relacionadas para obtener mejores respuestas",
"show_thinking": "Mostrar pensamiento",
"show_thinking_description": "Mostrar la cadena del proceso de pensamiento de la IA",
"enter_message": "Ingrese su mensaje...",
"error_contacting_provider": "Error al contactar con su proveedor de IA. Por favor compruebe sus ajustes y conexión a internet.",
"error_generating_response": "Error al generar respuesta de IA",
"index_all_notes": "Indexar todas las notas",
"index_status": "Estado de índice",
"indexed_notes": "Notas indexadas",
"indexing_stopped": "Indexado detenido",
"indexing_in_progress": "Indexado en progreso...",
"last_indexed": "Último indexado",
"n_notes_queued": "{{ count }} nota agregada a la cola para indexado",
"n_notes_queued_plural": "{{ count }} notas agregadas a la cola para indexado",
"note_chat": "Chat de nota",
"notes_indexed": "{{ count }} nota indexada",
"notes_indexed_plural": "{{ count }} notas indexadas",
"sources": "Fuentes",
"start_indexing": "Comenzar indexado",
"use_advanced_context": "Usar contexto avanzado",
"ollama_no_url": "Ollama no está configurado. Por favor ingrese una URL válida.",
"chat": {
"root_note_title": "Chats de IA",
"root_note_content": "Esta nota contiene tus conversaciones de chat de IA guardadas.",
"new_chat_title": "Nuevo chat",
"create_new_ai_chat": "Crear nuevo chat de IA"
},
"create_new_ai_chat": "Crear nuevo chat de IA",
"configuration_warnings": "Hay algunos problemas con su configuración de IA. Por favor compruebe sus ajustes.",
"experimental_warning": "La característica de LLM aún es experimental - ha sido advertido.",
"selected_provider": "Proveedor seleccionado",
"selected_provider_description": "Elija el proveedor de IA para el chat y características de completado",
"select_model": "Seleccionar modelo...",
"select_provider": "Seleccionar proveedor..."
},
"zoom_factor": { "zoom_factor": {
"title": "Factor de zoom (solo versión de escritorio)", "title": "Factor de zoom (solo versión de escritorio)",
"description": "El zoom también se puede controlar con los atajos CTRL+- y CTRL+=." "description": "El zoom también se puede controlar con los atajos CTRL+- y CTRL+=."
@ -1236,12 +1382,26 @@
"label": "Tamaño para modo de solo lectura automático (notas de texto)", "label": "Tamaño para modo de solo lectura automático (notas de texto)",
"unit": "caracteres" "unit": "caracteres"
}, },
"custom_date_time_format": {
"title": "Formato de fecha/hora personalizada",
"description": "Personalizar el formado de fecha y la hora insertada vía <kbd></kbd> o la barra de herramientas. Véa la <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">documentación de Day.js</a> para más tokens de formato disponibles.",
"format_string": "Cadena de formato:",
"formatted_time": "Fecha/hora personalizada:"
},
"i18n": { "i18n": {
"title": "Localización", "title": "Localización",
"language": "Idioma", "language": "Idioma",
"first-day-of-the-week": "Primer día de la semana", "first-day-of-the-week": "Primer día de la semana",
"sunday": "Domingo", "sunday": "Domingo",
"monday": "Lunes" "monday": "Lunes",
"first-week-of-the-year": "Primer semana del año",
"first-week-contains-first-day": "Primer semana que contiene al primer día del año",
"first-week-contains-first-thursday": "Primer semana que contiene al primer jueves del año",
"first-week-has-minimum-days": "Primer semana que contiene un mínimo de días",
"min-days-in-first-week": "Días mínimos en la primer semana",
"first-week-info": "Primer semana que contiene al primer jueves del año está basado en el estándar<a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
"first-week-warning": "Cambiar las opciones de primer semana puede causar duplicados con las Notas Semanales existentes y las Notas Semanales existentes no serán actualizadas respectivamente.",
"formatting-locale": "Fecha y formato de número"
}, },
"backup": { "backup": {
"automatic_backup": "Copia de seguridad automática", "automatic_backup": "Copia de seguridad automática",
@ -1308,6 +1468,39 @@
"password_mismatch": "Las nuevas contraseñas no son las mismas.", "password_mismatch": "Las nuevas contraseñas no son las mismas.",
"password_changed_success": "La contraseña ha sido cambiada. Trilium se recargará después de presionar Aceptar." "password_changed_success": "La contraseña ha sido cambiada. Trilium se recargará después de presionar Aceptar."
}, },
"multi_factor_authentication": {
"title": "Autenticación Multi-Factor",
"description": "La autenticación multifactor (MFA) agrega una capa adicional de seguridad a su cuenta. En lugar de solo ingresar una contraseña para iniciar sesión, MFA requiere que proporcione una o más pruebas adicionales para verificar su identidad. De esta manera, incluso si alguien se apodera de su contraseña, aún no puede acceder a su cuenta sin la segunda pieza de información. Es como agregar una cerradura adicional a su puerta, lo que hace que sea mucho más difícil para cualquier otra persona entrar.<br><br>Por favor siga las instrucciones a continuación para habilitar MFA. Si no lo configura correctamente, el inicio de sesión volverá a solo contraseña.",
"mfa_enabled": "Habilitar la autenticación multifactor",
"mfa_method": "Método MFA",
"electron_disabled": "Actualmente la autenticación multifactor no está soportada en la compilación de escritorio.",
"totp_title": "Contraseña de un solo uso basada en el tiempo (TOTP)",
"totp_description": "TOTP (contraseña de un solo uso basada en el tiempo) es una característica de seguridad que genera un código temporal único que cambia cada 30 segundos. Utiliza este código, junto con su contraseña para iniciar sesión en su cuenta, lo que hace que sea mucho más difícil para cualquier otra persona acceder a ella.",
"totp_secret_title": "Generar secreto TOTP",
"totp_secret_generate": "Generar secreto TOTP",
"totp_secret_regenerate": "Regenerar secreto TOTP",
"no_totp_secret_warning": "Para habilitar TOTP, primero debe de generar un secreto TOTP.",
"totp_secret_description_warning": "Después de generar un nuevo secreto TOTP, le será requerido que inicie sesión otra vez con el nuevo secreto TOTP.",
"totp_secret_generated": "Secreto TOTP generado",
"totp_secret_warning": "Por favor guarde el secreto generado en una ubicación segura. No será mostrado de nuevo.",
"totp_secret_regenerate_confirm": "¿Está seguro que desea regenerar el secreto TOTP? Esto va a invalidar el secreto TOTP previo y todos los códigos de recuperación existentes.",
"recovery_keys_title": "Claves de recuperación para un solo inicio de sesión",
"recovery_keys_description": "Las claves de recuperación para un solo inicio de sesión son usadas para iniciar sesión incluso cuando no puede acceder a los códigos de su autentificador.",
"recovery_keys_description_warning": "Las claves de recuperación no son mostrada de nuevo después de dejar esta página, manténgalas en un lugar seguro.<br>Después de que una clave de recuperación es utilizada ya no puede utilizarse de nuevo.",
"recovery_keys_error": "Error al generar códigos de recuperación",
"recovery_keys_no_key_set": "No hay códigos de recuperación establecidos",
"recovery_keys_generate": "Generar códigos de recuperación",
"recovery_keys_regenerate": "Regenerar códigos de recuperación",
"recovery_keys_used": "Usado: {{date}}",
"recovery_keys_unused": "El código de recuperación {{index}} está sin usar",
"oauth_title": "OAuth/OpenID",
"oauth_description": "OpenID es una forma estandarizada de permitirle iniciar sesión en sitios web utilizando una cuenta de otro servicio, como Google, para verificar su identidad. Siga estas <a href = \"https://developers.google.com/identity/openid-connect/openid-connect\">instrucciones</a> para configurar un servicio OpenID a través de Google.",
"oauth_description_warning": "Para habilitar OAuth/OpenID, necesita establecer la URL base de OAuth/OpenID, ID de cliente y secreto de cliente en el archivo config.ini y reiniciar la aplicación. Si desea establecerlas desde variables de ambiente, por favor establezca TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID y TRILIUM_OAUTH_CLIENT_SECRET.",
"oauth_missing_vars": "Ajustes faltantes: {{variables}}",
"oauth_user_account": "Cuenta de usuario: ",
"oauth_user_email": "Correo electrónico de usuario: ",
"oauth_user_not_logged_in": "¡No ha iniciado sesión!"
},
"shortcuts": { "shortcuts": {
"keyboard_shortcuts": "Atajos de teclado", "keyboard_shortcuts": "Atajos de teclado",
"multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.", "multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.",
@ -1400,7 +1593,7 @@
"move-to": "Mover a...", "move-to": "Mover a...",
"paste-into": "Pegar en", "paste-into": "Pegar en",
"paste-after": "Pegar después de", "paste-after": "Pegar después de",
"duplicate-subtree": "Duplicar subárbol", "duplicate": "Duplicar",
"export": "Exportar", "export": "Exportar",
"import-into-note": "Importar a nota", "import-into-note": "Importar a nota",
"apply-bulk-actions": "Aplicar acciones en lote", "apply-bulk-actions": "Aplicar acciones en lote",
@ -1419,7 +1612,7 @@
"relation-map": "Mapa de Relaciones", "relation-map": "Mapa de Relaciones",
"note-map": "Mapa de Notas", "note-map": "Mapa de Notas",
"render-note": "Nota de Renderizado", "render-note": "Nota de Renderizado",
"book": "Libro", "book": "",
"mermaid-diagram": "Diagrama Mermaid", "mermaid-diagram": "Diagrama Mermaid",
"canvas": "Lienzo", "canvas": "Lienzo",
"web-view": "Vista Web", "web-view": "Vista Web",
@ -1431,7 +1624,9 @@
"widget": "Widget", "widget": "Widget",
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?", "confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?",
"geo-map": "Mapa Geo", "geo-map": "Mapa Geo",
"beta-feature": "Beta" "beta-feature": "Beta",
"ai-chat": "Chat de IA",
"task-list": "Lista de tareas"
}, },
"protect_note": { "protect_note": {
"toggle-on": "Proteger la nota", "toggle-on": "Proteger la nota",
@ -1541,7 +1736,9 @@
}, },
"clipboard": { "clipboard": {
"cut": "La(s) notas(s) han sido cortadas al portapapeles.", "cut": "La(s) notas(s) han sido cortadas al portapapeles.",
"copied": "La(s) notas(s) han sido copiadas al portapapeles." "copied": "La(s) notas(s) han sido copiadas al portapapeles.",
"copy_failed": "No se puede copiar al portapapeles debido a problemas de permisos.",
"copy_success": "Copiado al portapapeles."
}, },
"entrypoints": { "entrypoints": {
"note-revision-created": "Una revisión de nota ha sido creada.", "note-revision-created": "Una revisión de nota ha sido creada.",
@ -1584,7 +1781,7 @@
"auto-detect-language": "Detectado automáticamente" "auto-detect-language": "Detectado automáticamente"
}, },
"highlighting": { "highlighting": {
"title": "", "title": "Bloques de código",
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.", "description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
"color-scheme": "Esquema de color" "color-scheme": "Esquema de color"
}, },
@ -1592,7 +1789,8 @@
"word_wrapping": "Ajuste de palabras", "word_wrapping": "Ajuste de palabras",
"theme_none": "Sin resaltado de sintaxis", "theme_none": "Sin resaltado de sintaxis",
"theme_group_light": "Temas claros", "theme_group_light": "Temas claros",
"theme_group_dark": "Temas oscuros" "theme_group_dark": "Temas oscuros",
"copy_title": "Copiar al portapapeles"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formato" "title": "Formato"
@ -1713,5 +1911,22 @@
}, },
"png_export_button": { "png_export_button": {
"button_title": "Exportar diagrama como PNG" "button_title": "Exportar diagrama como PNG"
},
"svg": {
"export_to_png": "El diagrama no pudo ser exportado a PNG."
},
"code_theme": {
"title": "Apariencia",
"word_wrapping": "Ajuste de palabras",
"color-scheme": "Esquema de color"
},
"cpu_arch_warning": {
"title": "Por favor descargue la versión ARM64",
"message_macos": "TriliumNext está siendo ejecutado bajo traducción Rosetta 2, lo que significa que está usando la versión Intel (x64) en Apple Silicon Mac. Esto impactará significativamente en el rendimiento y la vida de la batería.",
"message_windows": "TriliumNext está siendo ejecutado bajo emulación, lo que significa que está usando la version Intel (x64) en Windows en un dispositivo ARM. Esto impactará significativamente en el rendimiento y la vida de la batería.",
"recommendation": "Para la mejor experiencia, por favor descargue la versión nativa ARM64 de TriliumNext desde nuestra página de lanzamientos.",
"download_link": "Descargar versión nativa",
"continue_anyway": "Continuar de todas maneras",
"dont_show_again": "No mostrar esta advertencia otra vez"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "À propos de TriliumNext Notes", "title": "À propos de Trilium Notes",
"close": "Fermer", "close": "Fermer",
"homepage": "Page d'accueil :", "homepage": "Page d'accueil :",
"app_version": "Version de l'application :", "app_version": "Version de l'application :",
@ -639,7 +639,7 @@
"reload_frontend": "Recharger l'interface", "reload_frontend": "Recharger l'interface",
"show_hidden_subtree": "Afficher le Sous-arbre caché", "show_hidden_subtree": "Afficher le Sous-arbre caché",
"show_help": "Afficher l'aide", "show_help": "Afficher l'aide",
"about": "À propos de TriliumNext Notes", "about": "À propos de Trilium Notes",
"logout": "Déconnexion", "logout": "Déconnexion",
"show-cheatsheet": "Afficher l'aide rapide", "show-cheatsheet": "Afficher l'aide rapide",
"toggle-zen-mode": "Zen Mode" "toggle-zen-mode": "Zen Mode"
@ -753,7 +753,7 @@
"expand_all_children": "Développer tous les enfants", "expand_all_children": "Développer tous les enfants",
"collapse": "Réduire", "collapse": "Réduire",
"expand": "Développer", "expand": "Développer",
"book_properties": "Propriétés du livre", "book_properties": "",
"invalid_view_type": "Type de vue non valide '{{type}}'", "invalid_view_type": "Type de vue non valide '{{type}}'",
"calendar": "Calendrier" "calendar": "Calendrier"
}, },
@ -1389,7 +1389,7 @@
"move-to": "Déplacer vers...", "move-to": "Déplacer vers...",
"paste-into": "Coller dans", "paste-into": "Coller dans",
"paste-after": "Coller après", "paste-after": "Coller après",
"duplicate-subtree": "Dupliquer le sous-arbre", "duplicate": "Dupliquer",
"export": "Exporter", "export": "Exporter",
"import-into-note": "Importer dans la note", "import-into-note": "Importer dans la note",
"apply-bulk-actions": "Appliquer des Actions groupées", "apply-bulk-actions": "Appliquer des Actions groupées",
@ -1408,7 +1408,7 @@
"relation-map": "Carte des relations", "relation-map": "Carte des relations",
"note-map": "Carte de notes", "note-map": "Carte de notes",
"render-note": "Rendu Html", "render-note": "Rendu Html",
"book": "Livre", "book": "",
"mermaid-diagram": "Diagramme Mermaid", "mermaid-diagram": "Diagramme Mermaid",
"canvas": "Canevas", "canvas": "Canevas",
"web-view": "Affichage Web", "web-view": "Affichage Web",

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "Despre TriliumNext Notes", "title": "Despre Trilium Notes",
"homepage": "Site web:", "homepage": "Site web:",
"app_version": "Versiune aplicație:", "app_version": "Versiune aplicație:",
"db_version": "Versiune bază de date:", "db_version": "Versiune bază de date:",
@ -274,7 +274,7 @@
"no_children_help": "Această notiță de tip Carte nu are nicio subnotiță așadar nu este nimic de afișat. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pentru detalii." "no_children_help": "Această notiță de tip Carte nu are nicio subnotiță așadar nu este nimic de afișat. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pentru detalii."
}, },
"book_properties": { "book_properties": {
"book_properties": "Proprietăți carte", "book_properties": "",
"collapse": "Minimizează", "collapse": "Minimizează",
"collapse_all_notes": "Minimizează toate notițele", "collapse_all_notes": "Minimizează toate notițele",
"expand": "Expandează", "expand": "Expandează",
@ -572,7 +572,7 @@
"system-default": "Fontul predefinit al sistemului" "system-default": "Fontul predefinit al sistemului"
}, },
"global_menu": { "global_menu": {
"about": "Despre TriliumNext Notes", "about": "Despre Trilium Notes",
"advanced": "Opțiuni avansate", "advanced": "Opțiuni avansate",
"configure_launchbar": "Configurează bara de lansare", "configure_launchbar": "Configurează bara de lansare",
"logout": "Deautentificare", "logout": "Deautentificare",
@ -1349,7 +1349,7 @@
"copy-note-path-to-clipboard": "Copiază calea notiței în clipboard", "copy-note-path-to-clipboard": "Copiază calea notiței în clipboard",
"cut": "Decupează", "cut": "Decupează",
"delete": "Șterge", "delete": "Șterge",
"duplicate-subtree": "Dublifică ierarhia", "duplicate": "Dublifică",
"edit-branch-prefix": "Editează prefixul ramurii", "edit-branch-prefix": "Editează prefixul ramurii",
"expand-subtree": "Expandează subnotițele", "expand-subtree": "Expandează subnotițele",
"export": "Exportă", "export": "Exportă",
@ -1377,7 +1377,7 @@
"shared_publicly": "Această notiță este partajată public la" "shared_publicly": "Această notiță este partajată public la"
}, },
"note_types": { "note_types": {
"book": "Carte", "book": "Colecție",
"canvas": "Schiță", "canvas": "Schiță",
"code": "Cod sursă", "code": "Cod sursă",
"mermaid-diagram": "Diagramă Mermaid", "mermaid-diagram": "Diagramă Mermaid",

View File

@ -1,6 +1,6 @@
{ {
"about": { "about": {
"title": "關於 TriliumNext Notes", "title": "關於 Trilium Notes",
"homepage": "項目主頁:", "homepage": "項目主頁:",
"app_version": "軟件版本:", "app_version": "軟件版本:",
"db_version": "資料庫版本:", "db_version": "資料庫版本:",
@ -718,7 +718,7 @@
"expand_all_children": "展開所有子項", "expand_all_children": "展開所有子項",
"collapse": "折疊", "collapse": "折疊",
"expand": "展開", "expand": "展開",
"book_properties": "書籍屬性", "book_properties": "",
"invalid_view_type": "無效的查看類型 '{{type}}'" "invalid_view_type": "無效的查看類型 '{{type}}'"
}, },
"edited_notes": { "edited_notes": {
@ -1336,7 +1336,6 @@
"move-to": "移動到...", "move-to": "移動到...",
"paste-into": "貼上到裡面", "paste-into": "貼上到裡面",
"paste-after": "貼上到後面", "paste-after": "貼上到後面",
"duplicate-subtree": "複製子樹",
"export": "匯出", "export": "匯出",
"import-into-note": "匯入到筆記", "import-into-note": "匯入到筆記",
"apply-bulk-actions": "應用批量操作", "apply-bulk-actions": "應用批量操作",
@ -1355,7 +1354,7 @@
"relation-map": "關係圖", "relation-map": "關係圖",
"note-map": "筆記地圖", "note-map": "筆記地圖",
"render-note": "渲染筆記", "render-note": "渲染筆記",
"book": "", "book": "",
"mermaid-diagram": "美人魚圖Mermaid", "mermaid-diagram": "美人魚圖Mermaid",
"canvas": "畫布", "canvas": "畫布",
"web-view": "網頁視圖", "web-view": "網頁視圖",

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