Compare commits

...

601 Commits

Author SHA1 Message Date
renovate[bot]
2c4fb6c0d0 chore(deps): update node.js to v24.13.0 2026-01-14 01:02:53 +00:00
Elian Doran
ed1bf17add feat(i18n): add workflow to check translation coverage (#8376) 2026-01-13 23:06:10 +02:00
Elian Doran
4800f2a172 chore(ci/i18n): add permissions 2026-01-13 22:56:27 +02:00
Elian Doran
e7ff364c01 chore(i18n): trigger on workflow change 2026-01-13 22:52:51 +02:00
Elian Doran
79ca299726 feat(i18n): add workflow to check translation coverage 2026-01-13 22:49:46 +02:00
Elian Doran
9d7ba48a6a Translations update from Hosted Weblate (#8375) 2026-01-13 22:28:17 +02:00
Gishky
9329665919 Translated using Weblate (English (United Kingdom))
Currently translated at 1.9% (34 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/en_GB/
2026-01-13 21:23:46 +01:00
Gishky
f6821bce03 Translated using Weblate (German)
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-13 21:23:45 +01:00
Gishky
a7aedf93ab Translated using Weblate (German)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2026-01-13 21:23:45 +01:00
Elian Doran
4ffcf01452 fix: mac alt shortcuts (#8369) 2026-01-13 22:23:38 +02:00
Elian Doran
618459d353 feat(flake): provide .#edit-docs as a standalone docs editor (#8350) 2026-01-13 22:22:58 +02:00
Elian Doran
6436c16449 Fix race condition in edit-docs Electron ready event (#8344) 2026-01-13 22:20:22 +02:00
Elian Doran
1bec457004 Translations update from Hosted Weblate (#8374) 2026-01-13 21:58:13 +02:00
Hosted Weblate
3e541e37fe Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-13 20:44:40 +01:00
Elian Doran
a41b78d36f feat(ux): show more helpful output when users encounter permissions issues within the data directory (#8273) 2026-01-13 21:44:30 +02:00
Elian Doran
79c50f3b4c fix(deps): update dependency react-i18next to v16.5.2 (#8348) 2026-01-13 13:38:55 +02:00
Elian Doran
0f54b01cdd chore(deps): update dependency @smithy/middleware-retry to v4.4.20 (#8362) 2026-01-13 10:45:41 +02:00
Elian Doran
41f2b03711 chore(deps): update dependency @redocly/cli to v2.14.5 (#8361) 2026-01-13 10:33:06 +02:00
Elian Doran
0be76f982c Merge branch 'main' into renovate/smithy-middleware-retry-4.x 2026-01-13 10:33:02 +02:00
Elian Doran
5deb277672 chore(deps): update vitest monorepo to v4.0.17 (#8363) 2026-01-13 10:32:25 +02:00
Elian Doran
3d15c9e94c Translations update from Hosted Weblate (#8370) 2026-01-13 10:22:31 +02:00
noobhjy
1363f94621 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-13 08:14:58 +00:00
Kim Nøglegaard
abdcd6cc0c Translated using Weblate (Norwegian Bokmål)
Currently translated at 6.4% (25 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-13 08:14:58 +00:00
nvno
dc8abed2f3 Translated using Weblate (Portuguese)
Currently translated at 99.6% (1753 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt/
2026-01-13 08:14:57 +00:00
Kim Nøglegaard
892c2cd838 Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.3% (41 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-13 08:14:56 +00:00
Hosted Weblate
2796b29138 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-13 08:14:55 +00:00
Elian Doran
05620a129f chore(deps): update electron-forge monorepo to v7.11.1 (#8366) 2026-01-13 10:14:48 +02:00
renovate[bot]
cb11955a44 chore(deps): update vitest monorepo to v4.0.17 2026-01-13 08:12:19 +00:00
Elian Doran
c3623a15fb fix(ci): website workflow failing due to postinstall scripts 2026-01-13 10:09:51 +02:00
Wael Nasreddine
0273c64bbf Build edit-docs as standalone package using makeApp
Changed edit-docs from a simple wrapper script to a properly built Nix
package using makeApp, similar to how desktop and server are built.

Changes:
- Added build script to apps/edit-docs/package.json
- Created apps/edit-docs/scripts/build.ts based on desktop's build script
- Added edit-docs:build task to root package.json
- Changed flake.nix to use makeApp which:
  - Builds edit-docs with all dependencies bundled
  - Creates a standalone trilium-edit-docs executable
  - Can be installed with 'nix profile install' and run from any directory

This makes edit-docs truly reusable - it can now be installed and run
from any project without requiring the Trilium source tree.
2026-01-12 22:08:27 -08:00
Wael Nasreddine
5b37140ffa Fix race condition in edit-docs Electron ready event
The edit-docs tool would hang on startup when the Electron 'ready' event
fired before the event listener was registered. This race condition occurred
because:

1. startElectron() creates a deferred promise and registers a 'ready' listener
2. The 'ready' event fires asynchronously at some point during app initialization
3. If async work (like config loading) delays the listener registration,
   Electron may already be ready when app.on('ready', ...) is called
4. Once fired, the 'ready' event doesn't fire again, leaving the listener
   waiting forever

The fix checks electron.app.isReady() before registering the listener:
- If already ready: execute the handler immediately
- If not ready: register the listener as before

This ensures the initialization sequence completes regardless of timing.

The issue became apparent while working on making edit-docs reusable from
other projects (see #8343), where config loading added enough async delay
to consistently trigger the race condition.

Related: https://github.com/TriliumNext/Trilium/issues/8343
2026-01-12 22:08:27 -08:00
Wael Nasreddine
fb4d63b049 Add --config, --help, and --version flags to edit-docs
- Implement --config (-c) flag to allow custom configuration file paths.
- Add --help (-h) flag to display tool usage and available options.
- Add --version (-v) flag to display the current Trilium version.
- Update electron-start.mts to correctly pass command-line arguments to Electron.
- Synchronize edit-docs version with the root package.json via update-version.ts.
- Resolve config paths relative to the configuration file's directory.

This makes edit-docs more robust and easier to use from external projects
and immutable environments like Nix.
2026-01-12 22:08:27 -08:00
Wael Nasreddine
015e41e792 fix(edit-docs): Minify meta for format==share 2026-01-12 22:08:27 -08:00
Wael Nasreddine
8e47f33329 Refactor edit-docs to use edit-docs-config.yaml
This removes hardcoded configuration from edit-docs.ts and replaces
it with dynamic loading from edit-docs-config.yaml.

Changes:
- Removed BASE_URL and NOTE_MAPPINGS constants
- Removed DOCS_ROOT and USER_GUIDE_ROOT environment variable dependencies
- Added js-yaml for YAML parsing
- Config paths are resolved relative to repository root

The tool now reads configuration from edit-docs-config.yaml, making it
easier to customize without code changes. The pnpm script is simplified
since it no longer needs to pass complex environment variables.
2026-01-12 22:08:27 -08:00
Elian Doran
ad4a8ec5f4 chore(deps): use pinned versions 2026-01-13 07:40:20 +02:00
renovate[bot]
56356f9c61 fix(deps): update dependency react-i18next to v16.5.2 2026-01-13 05:38:17 +00:00
Elian Doran
abda0f9111 chore(deps): update dependency happy-dom to v20.1.0 (#8307) 2026-01-13 07:35:35 +02:00
Elian Doran
c2a9d21198 fix(deps): update dependency i18next to v25.7.4 (#8305) 2026-01-13 07:34:54 +02:00
Elian Doran
59ffa1fa93 fix(deps): update dependency diff to v8.0.3 (#8364) 2026-01-13 07:33:16 +02:00
Elian Doran
d056185368 chore(deps): update dependency eslint-plugin-playwright to v2.5.0 (#8365) 2026-01-13 07:32:37 +02:00
Elian Doran
251c1f6471 chore(deps): update typescript-eslint monorepo to v8.53.0 (#8367) 2026-01-13 07:32:15 +02:00
Chloe Lee
9efca9827e Merge branch 'main' into fix/mac-alt-shortcuts 2026-01-13 10:54:30 +08:00
renovate[bot]
f5e2129ad4 chore(deps): update typescript-eslint monorepo to v8.53.0 2026-01-13 01:17:33 +00:00
renovate[bot]
6c8e6f2429 chore(deps): update electron-forge monorepo to v7.11.1 2026-01-13 01:16:59 +00:00
renovate[bot]
9b4b1a393e chore(deps): update dependency eslint-plugin-playwright to v2.5.0 2026-01-13 01:16:28 +00:00
renovate[bot]
028334407c fix(deps): update dependency diff to v8.0.3 2026-01-13 01:15:54 +00:00
renovate[bot]
b93540b40d chore(deps): update dependency @smithy/middleware-retry to v4.4.20 2026-01-13 01:14:27 +00:00
renovate[bot]
9e7eba5eab chore(deps): update dependency @redocly/cli to v2.14.5 2026-01-13 01:13:45 +00:00
Elian Doran
fcfa64ae52 Translations update from Hosted Weblate (#8358) 2026-01-12 19:29:13 +02:00
Hosted Weblate
62f5b800b6 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-12 17:28:06 +00:00
Elian Doran
5b910cce56 fix: add "latex" alias for math command (#8357) 2026-01-12 19:27:50 +02:00
chloelee767
a5e8c8f573 add tests 2026-01-13 00:05:07 +08:00
Atmois
2c8edb413e fix: add "latex" alias for math command 2026-01-12 16:02:45 +00:00
chloelee767
644cc27fa7 fix alt shortcuts on mac not triggering 2026-01-12 23:05:42 +08:00
Elian Doran
71d3eb4fde Translations update from Hosted Weblate (#8340) 2026-01-12 07:58:21 +02:00
Elian Doran
7c2340d60e Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-12 07:55:25 +02:00
Elian Doran
24013ef020 Apply suggestion from @gemini-code-assist[bot]
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-12 07:55:16 +02:00
renovate[bot]
72d9e846b7 fix(deps): update dependency i18next to v25.7.4 2026-01-12 05:55:15 +00:00
Yatrik Patel
b572ea0954 Translated using Weblate (Hindi)
Currently translated at 12.0% (14 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-12 06:51:45 +01:00
Francis C.
060257fa06 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-12 06:51:44 +01:00
Yatrik Patel
1c6bb0a20e Translated using Weblate (Hindi)
Currently translated at 6.9% (27 of 388 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-12 06:51:43 +01:00
Hosted Weblate
1479109582 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/
2026-01-12 06:51:42 +01:00
Yatrik Patel
13f4e38f48 Translated using Weblate (Hindi)
Currently translated at 6.6% (26 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-12 06:51:41 +01:00
Yatrik Patel
5cbde8d32a Translated using Weblate (Hindi)
Currently translated at 1.1% (20 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-12 06:51:40 +01:00
Yatrik Patel
f3e3ef2f7d Translated using Weblate (Hindi)
Currently translated at 37.5% (57 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-12 06:51:39 +01:00
Yatrik Patel
0a58f8108a Translated using Weblate (Hindi)
Currently translated at 10.3% (12 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-12 06:51:38 +01:00
green
768213438a Translated using Weblate (Japanese)
Currently translated at 100.0% (1759 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-12 06:51:38 +01:00
Kim Nøglegaard
00e0eb6f8a Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.8% (11 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-12 06:51:37 +01:00
Kim Nøglegaard
3abea13d79 Translated using Weblate (Norwegian Bokmål)
Currently translated at 1.8% (33 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-12 06:51:36 +01:00
noobhjy
67ab7f0c1e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1757 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-12 06:51:35 +01:00
Kim Nøglegaard
b38e8e27b2 Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.5% (10 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-12 06:51:34 +01:00
Kim Nøglegaard
a70c103b93 Translated using Weblate (Norwegian Bokmål)
Currently translated at 1.4% (26 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-12 06:51:34 +01:00
Yatrik Patel
b83c3090f7 Translated using Weblate (Hindi)
Currently translated at 1.0% (19 of 1759 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-12 06:51:33 +01:00
Bart Louwers
59ee38e7a6 Translated using Weblate (Dutch)
Currently translated at 23.0% (35 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nl/
2026-01-12 06:51:32 +01:00
Bart Louwers
890fe5929b Translated using Weblate (Dutch)
Currently translated at 4.2% (75 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nl/
2026-01-12 06:51:31 +01:00
nvno
56cc312565 Translated using Weblate (Portuguese)
Currently translated at 91.5% (1603 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pt/
2026-01-12 06:51:30 +01:00
Elian Doran
9dfd015a27 fix(deps): update dependency preact to v10.28.2 [security] (#8301) 2026-01-12 07:51:22 +02:00
Elian Doran
04618dcdab fix(deps): update dependency react-window to v2.2.5 (#8353) 2026-01-12 07:50:09 +02:00
Elian Doran
f408e15c32 chore(deps): update dependency typedoc to v0.28.16 (#8352) 2026-01-12 07:49:20 +02:00
renovate[bot]
00e60c147c fix(deps): update dependency react-window to v2.2.5 2026-01-12 00:58:16 +00:00
renovate[bot]
ad6fd64226 chore(deps): update dependency typedoc to v0.28.16 2026-01-12 00:57:28 +00:00
renovate[bot]
6595fd9c10 chore(deps): update dependency happy-dom to v20.1.0 2026-01-11 14:43:06 +00:00
Elian Doran
a268507b80 chore(deps): update dependency ws to v8.19.0 (#8276) 2026-01-11 16:41:38 +02:00
renovate[bot]
9e847f67f2 chore(deps): update dependency ws to v8.19.0 2026-01-11 14:27:35 +00:00
renovate[bot]
df4992122b fix(deps): update dependency preact to v10.28.2 [security] 2026-01-11 14:26:08 +00:00
Elian Doran
a325ba7b8f chore(deps): update dependency @types/node to v24.10.7 (#8347) 2026-01-11 16:23:31 +02:00
Elian Doran
26e64ae7d0 Lightweight: Bootstrap index EJS (#8283) 2026-01-11 15:45:25 +02:00
renovate[bot]
d793774f51 chore(deps): update dependency @types/node to v24.10.7 2026-01-11 13:32:09 +00:00
Elian Doran
c5f2b5c177 fix(deps): update dependency react-window to v2.2.4 (#8275) 2026-01-11 15:31:53 +02:00
Elian Doran
b2f782f2a3 fix(deps): update dependency node-html-parser to v7.0.2 (#8306) 2026-01-11 15:31:24 +02:00
Elian Doran
b65b31ca4d chore(deps): update dependency vite to v7.3.1 (#8304) 2026-01-11 15:29:23 +02:00
Elian Doran
64827dcdcf fix(deps): update dependency mind-elixir to v5.5.0 (#8278) 2026-01-11 15:28:42 +02:00
Elian Doran
96d5d07087 chore(deps): update dependency @redocly/cli to v2.14.4 (#8316) 2026-01-11 15:27:44 +02:00
Elian Doran
ffd0f4727a chore(deps): update typescript-eslint monorepo to v8.52.0 (#8277) 2026-01-11 15:26:38 +02:00
Elian Doran
e33cd86d30 chore(deps): update dependency supertest to v7.2.2 (#8291) 2026-01-11 15:25:53 +02:00
Elian Doran
fc85c23a67 fix(deps): update dependency @codemirror/view to v6.39.9 (#8289) 2026-01-11 15:25:17 +02:00
Elian Doran
a55c8fb210 chore(deps): update dependency sax to v1.4.4 (#8317) 2026-01-11 15:24:44 +02:00
Elian Doran
19b865a5b4 chore(deps): update dependency express-openid-connect to v2.19.4 (#8327) 2026-01-11 15:24:10 +02:00
Elian Doran
61d90dda36 chore(deps): update dependency @smithy/middleware-retry to v4.4.19 (#8303) 2026-01-11 14:15:07 +02:00
Elian Doran
a4b1f06475 chore(deps): update dependency eslint-plugin-playwright to v2.4.1 (#8326) 2026-01-11 14:14:10 +02:00
Elian Doran
49704ea928 chore(deps): update dependency openai to v6.16.0 (#8328) 2026-01-11 14:13:32 +02:00
Elian Doran
a73df362d5 fix(deps): update dependency better-sqlite3 to v12.6.0 (#8349) 2026-01-11 14:13:06 +02:00
renovate[bot]
f42010e22a fix(deps): update dependency mind-elixir to v5.5.0 2026-01-11 10:28:35 +00:00
renovate[bot]
0b85e0fe2d fix(deps): update dependency better-sqlite3 to v12.6.0 2026-01-11 10:27:40 +00:00
renovate[bot]
c7f0720237 chore(deps): update typescript-eslint monorepo to v8.52.0 2026-01-11 10:26:45 +00:00
renovate[bot]
cc3031eaad chore(deps): update dependency supertest to v7.2.2 2026-01-11 10:25:13 +00:00
renovate[bot]
2850c7808c chore(deps): update dependency openai to v6.16.0 2026-01-11 10:24:24 +00:00
renovate[bot]
30bbcd866f fix(deps): update dependency react-window to v2.2.4 2026-01-11 10:22:53 +00:00
renovate[bot]
55b6f322ac fix(deps): update dependency node-html-parser to v7.0.2 2026-01-11 10:21:22 +00:00
Elian Doran
4518c9bb99 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs 2026-01-11 12:20:17 +02:00
renovate[bot]
c551c863f4 fix(deps): update dependency @codemirror/view to v6.39.9 2026-01-11 10:19:51 +00:00
renovate[bot]
3c25f8b4f3 chore(deps): update dependency vite to v7.3.1 2026-01-11 10:19:14 +00:00
renovate[bot]
d3c37556c3 chore(deps): update dependency sax to v1.4.4 2026-01-11 10:18:42 +00:00
renovate[bot]
cf60fcd6c1 chore(deps): update dependency express-openid-connect to v2.19.4 2026-01-11 10:18:10 +00:00
renovate[bot]
97075ff91b chore(deps): update dependency eslint-plugin-playwright to v2.4.1 2026-01-11 10:17:37 +00:00
renovate[bot]
ce57a43d90 chore(deps): update dependency @smithy/middleware-retry to v4.4.19 2026-01-11 10:15:52 +00:00
renovate[bot]
33485369c3 chore(deps): update dependency @redocly/cli to v2.14.4 2026-01-11 10:15:00 +00:00
Elian Doran
7fcd93a61b e2e(server): fix broken e2e test after MathTex integration 2026-01-11 11:55:49 +02:00
Elian Doran
3d4b84c7c4 e2e(server): format file 2026-01-11 11:49:49 +02:00
Elian Doran
61eb4017dd Vite performance improvement (#8345) 2026-01-11 11:28:36 +02:00
Elian Doran
b4df4aaf0d chore(client): address requested changes 2026-01-11 11:06:59 +02:00
Elian Doran
244294f699 Revert "chore(scripts): remove analyze-perf"
This reverts commit 94d4a307cf.
2026-01-11 10:55:47 +02:00
Elian Doran
d609ee028e chore(client): get rid of workspace projects from optimizeDeps 2026-01-11 10:55:13 +02:00
Elian Doran
8dc8b046fb chore(client): reintroduce live refresh 2026-01-11 00:55:42 +02:00
Elian Doran
349203e300 chore(client): remove dependency to @preact/preset-vite 2026-01-11 00:35:11 +02:00
Elian Doran
83a8f07998 chore(client): change vite config 2026-01-11 00:33:40 +02:00
Elian Doran
94d4a307cf chore(scripts): remove analyze-perf 2026-01-11 00:21:38 +02:00
Elian Doran
2e35e0a830 chore(client): re-enable source maps 2026-01-11 00:21:32 +02:00
Elian Doran
ecdb819067 chore(scripts): address requested changes 2026-01-11 00:11:13 +02:00
Elian Doran
e2cf0c6e3e chore(client): disable preact preset-vite 2026-01-10 23:27:14 +02:00
Elian Doran
5dd600a291 chore(client): shared config 2026-01-10 23:09:51 +02:00
Elian Doran
df1beb1ffb chore(client): set up LightningCSS 2026-01-10 23:04:04 +02:00
Elian Doran
7773059ac0 chore(client): optimize babel 2026-01-10 22:40:20 +02:00
Elian Doran
a238fc16b2 chore(client): add more optimize deps 2026-01-10 22:26:01 +02:00
Elian Doran
e298f5ea6f chore(client): optimize dependencies more aggressively 2026-01-10 22:12:41 +02:00
Elian Doran
a5512267c1 feat(ckeditor5): lazy-load premium features 2026-01-10 21:54:35 +02:00
Elian Doran
f503c4ca6c feat(ckeditor5-math): use dynamic import to reduce loading time 2026-01-10 21:49:16 +02:00
Elian Doran
c834c01c8e chore(scripts): improve performance analysis script by timing out earlier 2026-01-10 21:48:27 +02:00
Elian Doran
1f72ab9593 chore(scripts): spawn process automatically 2026-01-10 21:20:13 +02:00
Elian Doran
c7d446f4aa chore(scripts): display time in seconds 2026-01-10 21:13:34 +02:00
Elian Doran
fb1530423d chore(scripts): add more categories to analyze performance 2026-01-10 21:12:35 +02:00
Elian Doran
545464efee chore(scripts): add a script to anlayze performance logs 2026-01-10 21:10:56 +02:00
Elian Doran
29d0223fd1 chore(server): address requested changes 2026-01-10 20:45:03 +02:00
Elian Doran
00852277f2 fix(server): some routes not working on prod 2026-01-10 20:43:43 +02:00
Elian Doran
edfe23d88c fix(server): server-side images not served in dev mode 2026-01-10 19:58:05 +02:00
Elian Doran
2b8a7a28d9 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs 2026-01-10 19:51:05 +02:00
Elian Doran
7f29480237 Revert "refactor(client): get rid of runtime in favor of bootstrap script"
This reverts commit 2e845a9faa.
2026-01-10 19:51:03 +02:00
Elian Doran
61ac482946 Revert "chore(server): remove runtime from login"
This reverts commit 455edbfb5d.
2026-01-10 19:48:14 +02:00
Elian Doran
de6a6cbb07 feat(tree): open notes in new window from tree (#8269) 2026-01-10 19:37:12 +02:00
Elian Doran
2dcb003909 chore(deps): update pnpm to v10.28.0 (#8329) 2026-01-10 19:36:34 +02:00
Elian Doran
d5c934a518 add logseq to supported protocols (#8320) 2026-01-10 19:36:03 +02:00
Elian Doran
779909837c Allow hiding children from tree (especially for collections) (#8335) 2026-01-10 18:32:41 +02:00
Elian Doran
a4cb375a0f test(client): fix broken tests 2026-01-10 18:21:38 +02:00
Elian Doran
e2da8c28ca fix(tree): spotlighted note has + button & insert child context menu 2026-01-10 18:21:16 +02:00
Elian Doran
7808848f05 Translations update from Hosted Weblate (#8339) 2026-01-10 18:02:07 +02:00
Elian Doran
7c05109645 Merge remote-tracking branch 'origin/main' into feature/hide_from_tree 2026-01-10 18:01:05 +02:00
Hosted Weblate
8f9e89b73b Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-10 15:59:25 +00:00
Elian Doran
861a61a4d8 Tree performance improvement (#8274) 2026-01-10 17:59:08 +02:00
Elian Doran
52d4083814 chore(tree): address requested changes 2026-01-10 17:46:45 +02:00
Elian Doran
0272189b22 fix(tree): performance issue due to batch update 2026-01-10 17:32:06 +02:00
Elian Doran
ddba0e823c fix(tree): tree is updated on note content updates 2026-01-10 17:32:02 +02:00
Elian Doran
be81acb9e7 feat(tree): respect motion settings instead of always disabling animation 2026-01-10 16:11:15 +02:00
Elian Doran
3bb97385c9 fix(client): a case where inheritance boolean is not correct 2026-01-10 13:17:44 +02:00
Elian Doran
a72cec0494 chore(client): address a few requested changes 2026-01-10 13:06:13 +02:00
Elian Doran
cb02198c6f docs(user): document hiding the subtree 2026-01-10 12:57:11 +02:00
Elian Doran
298d438230 Merge branch 'feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-10 12:34:44 +02:00
Elian Doran
cb2f7932dd Merge remote-tracking branch 'origin/main' into feature/tree_performance_improvement 2026-01-10 12:34:26 +02:00
Elian Doran
3354bd669f fix(client/tree): toast displayed when doing operations outside of tree 2026-01-10 12:32:55 +02:00
Elian Doran
8ad779be66 fix(client/load_results): component ID not preserved for attributes & branches 2026-01-10 12:26:08 +02:00
Elian Doran
f462034868 fix(client/tree): toast not appearing when inserted via paste 2026-01-10 11:55:49 +02:00
Elian Doran
26c25cd4cd fix(client): edge case not handled when parent note overrides to false 2026-01-10 11:25:00 +02:00
Elian Doran
6398830c2d test(client): attribute toggling 2026-01-10 11:15:21 +02:00
Elian Doran
0b065063f2 refactor(client): use same logic for setting boolean with inheritance 2026-01-10 10:37:19 +02:00
Elian Doran
a3a9de6fdd feat(collections): add setting to hide subtree 2026-01-10 10:34:06 +02:00
Elian Doran
d77d30f29e fix(note_tree): subtree hidden cannot be overridden through inheritance 2026-01-10 10:17:09 +02:00
Elian Doran
5cabc6379d fix(note_tree): not reacting to changes in subtreeHidden 2026-01-10 10:14:15 +02:00
Elian Doran
af537e6a48 feat(tree): add option to hide or show subtree 2026-01-10 10:08:50 +02:00
Elian Doran
faf3797663 feat(tree): disable insert child note if subnotes are hidden 2026-01-10 09:57:09 +02:00
Elian Doran
db57f3ff62 feat(tree): add tooltip when moving into hidden subtree 2026-01-10 09:53:23 +02:00
Elian Doran
0f77caad69 feat(tree): hide items dragged into a subtreeHidden 2026-01-10 09:40:19 +02:00
renovate[bot]
751b91e1b8 chore(deps): update pnpm to v10.28.0 2026-01-10 00:41:10 +00:00
Elian Doran
968a17fbfb fix(tree): alignment of note count badge 2026-01-10 00:55:33 +02:00
Elian Doran
712e87b39f feat(tree): distinguish spotlighted node 2026-01-10 00:52:11 +02:00
Elian Doran
0b40b42315 fix(tree): random exceptions when switching between two spotlighted notes 2026-01-10 00:42:24 +02:00
Elian Doran
62996b1162 feat(tree): remove spotlighted note after switching to another one 2026-01-10 00:38:28 +02:00
Elian Doran
b67ccc6091 feat(tree): basic spotlight support for hidden child 2026-01-10 00:15:59 +02:00
Elian Doran
211d2dcf99 feat(collections): hide children by default for some collection types 2026-01-09 20:06:49 +02:00
Elian Doran
ee52e16a75 feat(tree): add title for subnote count badge 2026-01-09 19:15:57 +02:00
Elian Doran
0c27bd25fa chore(tree): align child count to the right 2026-01-09 17:03:56 +02:00
Elian Doran
b6a6e78d01 feat(tree): hide add button if subtree is hidden 2026-01-09 16:59:55 +02:00
Elian Doran
92e6a29e70 feat(tree): display number of children if subtree is hidden 2026-01-09 16:54:04 +02:00
Elian Doran
acc8cee7cd Merge remote-tracking branch 'origin/feature/tree_performance_improvement' into feature/hide_from_tree 2026-01-09 16:38:56 +02:00
Elian Doran
afefbe154b feat(tree): hide arrow if children are hidden 2026-01-09 16:37:11 +02:00
SngAbc
83fa55b7d9 Merge branch 'main' into feat/tree/new_window 2026-01-09 22:35:30 +08:00
Elian Doran
4f6c10d995 feat(tree): allow hiding child notes via attribute 2026-01-09 16:32:55 +02:00
Elian Doran
ed972d2601 e2e(server): math popup fails on CI 2026-01-09 12:13:39 +02:00
Elian Doran
6b57ee5654 e2e(server): flakiness in tab_bar 2026-01-09 11:54:16 +02:00
contributor
e469af1ca5 add logseq to supported protocols 2026-01-09 11:51:49 +02:00
Elian Doran
6d41f076c2 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-09 11:25:26 +02:00
Elian Doran
8cff591746 fix(toc): unnecessary <ol> when no children 2026-01-09 11:25:24 +02:00
Elian Doran
b3ccf89094 Translations update from Hosted Weblate (#8315) 2026-01-09 00:06:09 +02:00
Ulices
d31c6b1627 Translated using Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/es/
2026-01-08 23:01:53 +01:00
Yatrik Patel
1481356d1f Translated using Weblate (Hindi)
Currently translated at 36.8% (56 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-08 23:01:52 +01:00
Ulices
a54661fd0a Translated using Weblate (Spanish)
Currently translated at 93.8% (1643 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-08 23:01:49 +01:00
Elian Doran
ae4a3f10ae fix(toc): equations not rendered in new layout 2026-01-08 21:26:04 +02:00
Elian Doran
fe3160e7a1 e2e(server): adapt tests to new layout directly 2026-01-08 20:32:54 +02:00
Elian Doran
66659d4786 e2e(server): flaky test in PDF 2026-01-08 20:11:21 +02:00
Elian Doran
0b25b09040 feat(ci): check version consistency before releasing 2026-01-08 19:49:29 +02:00
Elian Doran
0d41cc2660 Merge remote-tracking branch 'origin/stable' 2026-01-08 19:42:10 +02:00
Elian Doran
f5e8822718 chore(release): prepare for v0.101.3 2026-01-08 19:38:21 +02:00
Elian Doran
bdc220ec12 Merge remote-tracking branch 'origin/stable' 2026-01-08 18:19:16 +02:00
Elian Doran
3eb68e5271 Stable fixes (#8310) 2026-01-08 18:16:55 +02:00
Elian Doran
521952ebcc test(client): remove debug statements 2026-01-08 18:10:00 +02:00
Elian Doran
034091a696 docs(release): prepare for v0.101.2 2026-01-08 18:08:34 +02:00
Elian Doran
ae881101d8 fix(note_list): archived notes displayed in empty grid card (closes #8184) 2026-01-08 17:23:40 +02:00
Elian Doran
b11a30c49c fix(launcher_bar): crashing if there is a non-launcher note (closes #8218) 2026-01-08 16:55:51 +02:00
Elian Doran
4625efda7f fix(note_list): skip rendering of included notes for performance (closes #8017) 2026-01-08 16:50:27 +02:00
Elian Doran
3c168d750d fix(client): cycle in include causing infinite loop (closes #8294) 2026-01-08 16:44:35 +02:00
Elian Doran
5cc7b259ce fix(client): max content width not preserved (closes #8065) 2026-01-08 15:59:57 +02:00
Elian Doran
f7ae046b20 fix(mermaid): error container not scrollable (closes #8299) 2026-01-08 15:52:19 +02:00
Elian Doran
02f43d6239 fix(mermaid): code not scrollable (closes #8299) 2026-01-08 15:33:16 +02:00
Elian Doran
53e1fa1047 fix(mermaid) diagrams not saving content and SVG attachment (#8220) 2026-01-08 15:22:07 +02:00
Elian Doran
b1dc0e234f fix(popupEditor): fix closing of popupEditor when inserting note link (#8224) 2026-01-08 15:21:23 +02:00
Elian Doran
9d380dd828 fix(sql_console): cannot copy table data (#8268) 2026-01-08 15:20:36 +02:00
Elian Doran
1f77540dbb fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-08 15:20:14 +02:00
Elian Doran
455edbfb5d chore(server): remove runtime from login 2026-01-07 23:31:28 +02:00
Elian Doran
7288b66d27 chore(client): address requested changes 2026-01-07 23:04:34 +02:00
Elian Doran
3d72ec80bb refactor(client): get rid of any 2026-01-07 22:01:35 +02:00
Elian Doran
f2a74df511 feat(client): use hashes for assets 2026-01-07 21:49:05 +02:00
Elian Doran
68c6052d10 chore(client): remove useless manual chunk 2026-01-07 21:39:35 +02:00
Elian Doran
c4edb56bd4 fix(server): not starting due to serving of assets 2026-01-07 21:38:44 +02:00
Elian Doran
b6a3fe7cfb chore(client): get rid of translation issue 2026-01-07 21:18:09 +02:00
Elian Doran
7a088c5b7d refactor(client): handle everything in bootstrap 2026-01-07 21:11:38 +02:00
Elian Doran
2e845a9faa refactor(client): get rid of runtime in favor of bootstrap script 2026-01-07 21:08:19 +02:00
Elian Doran
ac3ae0dbbe chore(client): fix type issues 2026-01-07 21:02:27 +02:00
Elian Doran
a3fc13de3a refactor(client): extract bootstrap script into separate file 2026-01-07 21:00:40 +02:00
Elian Doran
ee6cbc710c chore(server): remove font size globs 2026-01-07 20:52:27 +02:00
Elian Doran
18d701525e fix(client): print broken due to lack of query forwarding
; Conflicts:
;	apps/client/src/index.html
2026-01-07 20:52:04 +02:00
Elian Doran
e47c848ec8 chore(server): reintegrate mobile layout 2026-01-07 20:51:33 +02:00
Elian Doran
cd64548299 fix(client): load custom fonts 2026-01-07 20:51:29 +02:00
Elian Doran
8645d053de fix(client): ckeditor theme not loaded properly 2026-01-07 20:51:24 +02:00
Elian Doran
91f2dabed7 Merge remote-tracking branch 'origin/main' into lightweight/bootstrap_ejs
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 20:51:17 +02:00
Elian Doran
716612680d Translations update from Hosted Weblate (#8293) 2026-01-07 19:35:42 +02:00
Michael
3800fb85eb Translated using Weblate (German)
Currently translated at 95.4% (1672 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-07 18:32:56 +01:00
Rafa Osuna
d807984be4 Translated using Weblate (Spanish)
Currently translated at 92.7% (1624 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-01-07 18:32:56 +01:00
Giovi
2c92ae8898 Translated using Weblate (Italian)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-01-07 18:32:55 +01:00
Argann Bonneau
3d8cbc81c4 Translated using Weblate (French)
Currently translated at 94.5% (1656 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-07 18:32:54 +01:00
Yatrik Patel
d747c94450 Translated using Weblate (Hindi)
Currently translated at 3.4% (4 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-07 18:32:53 +01:00
pythaac
a627d1f96e Translated using Weblate (Korean)
Currently translated at 76.3% (116 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2026-01-07 18:32:53 +01:00
Yatrik Patel
869db5e478 Translated using Weblate (Hindi)
Currently translated at 0.9% (17 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-07 18:32:52 +01:00
Yatrik Patel
73e94d385e Translated using Weblate (Hindi)
Currently translated at 5.9% (23 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-07 18:32:51 +01:00
Kim Nøglegaard
8f4ebeb335 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-07 18:32:51 +01:00
Yatrik Patel
263ee864be Translated using Weblate (Hindi)
Currently translated at 9.2% (14 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-07 18:32:50 +01:00
Elian Doran
f078732624 fix(text): Title is not selected when creating a note via the launcher (#8292) 2026-01-07 19:32:37 +02:00
SngAbc
fac1f6b16c fix(text): Title is not focused when creating a note via the launcher
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-07 10:33:17 +08:00
SiriusXT
a5841c1423 fix(text): Title is not focused when creating a note via the launcher 2026-01-07 10:11:24 +08:00
Elian Doran
aaca18003d Translations update from Hosted Weblate (#8279) 2026-01-06 13:54:24 +02:00
Kim Nøglegaard
5ec521b024 Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (104 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-06 04:01:53 +01:00
Yatrik Patel
b3c0be7559 Translated using Weblate (Hindi)
Currently translated at 3.0% (12 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-06 04:01:51 +01:00
Máté Zsólya
d52b735b99 Translated using Weblate (Hungarian)
Currently translated at 1.9% (34 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hu/
2026-01-06 04:01:49 +01:00
Yatrik Patel
639b1f2863 Translated using Weblate (Hindi)
Currently translated at 5.9% (9 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-06 04:01:47 +01:00
Elian Doran
aff4f7e010 feat(tree): disable animation for performance 2026-01-06 01:34:02 +02:00
Elian Doran
dec4dafba6 feat(tree): avoid async 2026-01-06 01:26:56 +02:00
Elian Doran
d0cdcfc32c refactor(tree): use loop for mini optimisation 2026-01-06 01:21:43 +02:00
Elian Doran
0867b81c7a feat(tree): use template for create child to improve performance 2026-01-06 01:13:31 +02:00
Elian Doran
bde6068f2d refactor(tree): extract enchance title into separate method 2026-01-06 01:07:37 +02:00
Elian Doran
47fd2affa4 feat(tree): use direct DOM manipulation instead of jQuery 2026-01-06 00:59:32 +02:00
perfectra1n
2dd541e1d0 fix(tests): update data_dir tests for new EEXIST graceful handling 2026-01-05 14:34:52 -08:00
Elian Doran
7f2cc885fe Feat(math): Improve legacy math input with MathLive (#7842) 2026-01-06 00:12:38 +02:00
Elian Doran
19a365a370 fix(sql_console): cannot copy table data (#8268) 2026-01-06 00:10:11 +02:00
Elian Doran
9a50da328e chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 (#8265) 2026-01-05 23:53:05 +02:00
Elian Doran
181e36a7c1 Merge remote-tracking branch 'origin/main' into Meinzzzz/main
; Conflicts:
;	.gitignore
2026-01-05 23:46:12 +02:00
Elian Doran
178508d245 Merge branch 'main' into fix/sql_select_text 2026-01-05 23:43:29 +02:00
Elian Doran
8157ef5e74 Merge branch 'main' into feat/show-helpful-permission-error-output 2026-01-05 23:43:20 +02:00
Elian Doran
d132d084cf Merge branch 'main' into renovate/rollup-plugin-webpack-stats-2.x 2026-01-05 23:43:06 +02:00
Elian Doran
494b55d685 fix(ckeditor): missing pl locale 2026-01-05 23:39:36 +02:00
perfectra1n
0185dd0d18 feat(ux): implement suggestions from gemini just to make sure 2026-01-05 11:55:14 -08:00
perfectra1n
142ed42d90 feat(ux): show more helpful output when users encounter permissions issues within the data directory 2026-01-05 11:38:18 -08:00
Elian Doran
51513d3779 fix(status_bar): count not refreshing properly after change 2026-01-05 21:03:32 +02:00
SiriusXT
5b95b9875b feat(tree): open notes in new window from tree 2026-01-05 19:27:44 +08:00
Elian Doran
688d197472 chore(client): set up body classes 2026-01-05 11:55:51 +02:00
Elian Doran
b745fb476e chore(client): get icons to load 2026-01-05 11:50:21 +02:00
Elian Doran
047b5a85d2 chore(client): load stylesheets 2026-01-05 11:48:19 +02:00
Elian Doran
370a0c6a05 feat(client): get desktop to start 2026-01-05 11:40:53 +02:00
Elian Doran
0d4558fee1 feat(client): get glob to be populated 2026-01-05 11:37:03 +02:00
Elian Doran
76526e0a96 feat(server): bootstrap route 2026-01-05 11:22:10 +02:00
Elian Doran
70093e0a7d feat(server): render static Vite HTML 2026-01-05 11:07:40 +02:00
SngAbc
458398f2ca Merge branch 'main' into fix/sql_select_text 2026-01-05 13:51:45 +08:00
SngAbc
7a6cc4f51e fix(sql_console): cannot copy table data
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-05 12:16:16 +08:00
SiriusXT
f4ccce7de5 fix(sql_console): cannot copy table data 2026-01-05 11:23:50 +08:00
renovate[bot]
f8b5417d6c chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.9 2026-01-05 01:03:52 +00:00
Elian Doran
13ce8cf498 fix(note_list): the note list cannot open the context menu. (#8254) 2026-01-04 23:49:25 +02:00
Elian Doran
6c2afc086c feat(i18n): add Polish 2026-01-04 23:38:51 +02:00
Elian Doran
93d50712a9 chore(scripts): fix typecheck issue 2026-01-04 23:38:51 +02:00
Elian Doran
ed91a44928 feat(scripts): check translation coverage 2026-01-04 23:38:50 +02:00
Elian Doran
cd10e66fbb chore(scripts): build scripts not working properly on Windows 2026-01-04 23:38:50 +02:00
Elian Doran
d6aa126fcc Translations update from Hosted Weblate (#8264) 2026-01-04 22:34:38 +02:00
noobhjy
3308c7bdf4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-04 20:01:53 +00:00
Francis C.
56341a1a73 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-04 20:01:52 +00:00
green
0857e1a536 Translated using Weblate (Japanese)
Currently translated at 100.0% (1751 of 1751 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-04 20:01:50 +00:00
Kim Nøglegaard
5d6b25a29e Translated using Weblate (Norwegian Bokmål)
Currently translated at 57.2% (87 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-04 20:01:49 +00:00
Elian Doran
5bc15a5448 PDFjs v1 final tweaks (#8256) 2026-01-04 21:21:51 +02:00
Elian Doran
51a19d0544 docs(user): add missing slug 2026-01-04 21:11:13 +02:00
Elian Doran
fb4e912ed0 chore(pdfjs): address requested changes 2026-01-04 21:08:39 +02:00
Elian Doran
20c2652013 chore(server): set up environment for starting Nginx proxy with subdir 2026-01-04 20:33:54 +02:00
Elian Doran
971d6ad9e3 chore(pdfjs): use version-based system for cache busting 2026-01-04 20:06:11 +02:00
Elian Doran
757fc7a7fe chore(pdfjs): embed sandbox file 2026-01-04 18:50:40 +02:00
Elian Doran
e4d0a4554a feat(client/note_list): use built-in PDF viewer 2026-01-04 18:40:17 +02:00
Elian Doran
dfab7dbc4b fix(note_list): missing margin in button 2026-01-04 18:39:23 +02:00
Elian Doran
0039f4c155 feat(pdfjs): replace blob instead of creating a new revision every time 2026-01-04 17:25:56 +02:00
Elian Doran
23f7dc63b8 feat(pdfjs): enable editing features only if in main editor 2026-01-04 17:11:16 +02:00
Elian Doran
e485b75a44 fix(pdfjs): saves as soon as document is opened 2026-01-04 17:06:41 +02:00
Elian Doran
dbef57d329 chore(deps): update dependency webdriverio to v9.23.0 (#8258) 2026-01-04 10:36:47 +02:00
SiriusXT
c650441655 Merge branch 'main' into fix/note_list 2026-01-04 10:49:46 +08:00
SiriusXT
e573a8af77 chore(note_grid): remove unused tree import 2026-01-04 10:48:45 +08:00
SiriusXT
b23252d046 fix(note_grid): the note grid cannot open the context menu 2026-01-04 10:25:43 +08:00
renovate[bot]
2f7448dbd4 chore(deps): update dependency webdriverio to v9.23.0 2026-01-04 02:13:34 +00:00
Adorian Doran
9bf4aa2968 readme: update screenshot 2026-01-04 00:28:00 +02:00
Elian Doran
d78a7bad3b feat(import/markdown): handle bash as sh 2026-01-03 23:30:38 +02:00
Elian Doran
b812177e78 docs(user): add spellcheck=false to inline code 2026-01-03 23:28:28 +02:00
Elian Doran
4710a6af41 feat(export/markdown): add spellcheck=false to inline code 2026-01-03 23:19:58 +02:00
Elian Doran
a613980ea4 docs(user): add missing jsx / HTML code blocks 2026-01-03 22:56:23 +02:00
Elian Doran
20ae1f844b feat(markdown): support html, jsx in code blocks 2026-01-03 22:44:48 +02:00
Elian Doran
69511134e5 refactor(client/pdf): handle blob request on client side 2026-01-03 20:54:28 +02:00
Elian Doran
75952563e4 Translations update from Hosted Weblate (#8255) 2026-01-03 20:31:35 +02:00
Elian Doran
21cf5e1df7 chore(client/pdf): use custom spaced update hook 2026-01-03 20:29:54 +02:00
Hosted Weblate
9df5505989 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-01-03 18:03:22 +00:00
Kim Nøglegaard
1809d59193 Translated using Weblate (Norwegian Bokmål)
Currently translated at 43.4% (66 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-03 18:03:20 +00:00
Francis C.
feaa54d660 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1745 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-01-03 18:03:20 +00:00
noobhjy
c94bd41162 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1743 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-03 18:03:19 +00:00
Elian Doran
f1f3e66537 Save indicator (#8249) 2026-01-03 20:03:12 +02:00
Elian Doran
80363cdc73 chore(client/save_indicator): fix some spacing issues 2026-01-03 19:53:46 +02:00
Elian Doran
02e08fdf12 chore(client/save_indicator): address requested changes 2026-01-03 19:47:33 +02:00
Elian Doran
42283b2469 doc(user): mention the save status indicator 2026-01-03 19:40:39 +02:00
Elian Doran
d3b598a5b2 fix(client/save_indicator): not visible on light theme 2026-01-03 19:39:16 +02:00
Elian Doran
0dd3a03c6b chore(client): fix type issue 2026-01-03 19:30:52 +02:00
Elian Doran
2144888447 Merge remote-tracking branch 'origin/main' into feature/save_indicator 2026-01-03 19:24:51 +02:00
Elian Doran
b2549066dc PDF.js refinement (#8247) 2026-01-03 19:24:28 +02:00
Elian Doran
cd1f3aa9a7 chore(client): address self-review 2026-01-03 10:05:44 +02:00
Elian Doran
1674401342 Merge remote-tracking branch 'origin/main' into feature/pdfjs_refinement 2026-01-03 09:57:42 +02:00
Elian Doran
7ba8dbbf6e fix(deps): update dependency mind-elixir to v5.4.0 (#8253) 2026-01-03 09:53:36 +02:00
Elian Doran
ad27d9ed0e chore(deps): update dependency @redocly/cli to v2.14.3 (#8252) 2026-01-03 09:53:03 +02:00
renovate[bot]
482d2f9624 fix(deps): update dependency mind-elixir to v5.4.0 2026-01-03 01:46:19 +00:00
renovate[bot]
824ef704d4 chore(deps): update dependency @redocly/cli to v2.14.3 2026-01-03 01:45:28 +00:00
Adorian Doran
58b73cfc7d Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-01-03 02:25:07 +02:00
Adorian Doran
0465fea2db style/pdf viewer: improve appearance 2026-01-03 02:24:59 +02:00
Elian Doran
39b75e3561 Translations update from Hosted Weblate (#8248) 2026-01-03 00:10:42 +02:00
Elian Doran
2933db9b16 feat(save_indicator): fade out after a few seconds 2026-01-02 23:53:14 +02:00
Kim Nøglegaard
d94914046b Translated using Weblate (Norwegian Bokmål)
Currently translated at 23.0% (35 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:24 +00:00
Kim Nøglegaard
9cf384b14b Translated using Weblate (Norwegian Bokmål)
Currently translated at 20.3% (31 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:23 +00:00
Kim Nøglegaard
614a2f0ccb Translated using Weblate (Norwegian Bokmål)
Currently translated at 3.2% (5 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/nb_NO/
2026-01-02 21:51:23 +00:00
Yatrik Patel
5cecc72384 Translated using Weblate (Hindi)
Currently translated at 2.8% (11 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-02 21:51:22 +00:00
Yatrik Patel
3ad37fb602 Translated using Weblate (Hindi)
Currently translated at 5.2% (8 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-02 21:51:21 +00:00
noobhjy
42b048c2bf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (1742 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-01-02 21:51:21 +00:00
Kim Nøglegaard
a01bf3dfa1 Translated using Weblate (Norwegian Bokmål)
Currently translated at 2.0% (8 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/nb_NO/
2026-01-02 21:51:20 +00:00
Yatrik Patel
ad60988553 Translated using Weblate (Hindi)
Currently translated at 0.7% (13 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-02 21:51:19 +00:00
green
c9ae4e4cc6 Translated using Weblate (Japanese)
Currently translated at 100.0% (1745 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-01-02 21:51:18 +00:00
Kim Nøglegaard
d2639851d5 Translated using Weblate (Norwegian Bokmål)
Currently translated at 0.6% (11 of 1745 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nb_NO/
2026-01-02 21:51:18 +00:00
Hosted Weblate
8dc5f9cfa4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-02 21:51:17 +00:00
Adorian Doran
d99a408e04 style/pdf viewer: add support for background effects 2026-01-02 23:51:07 +02:00
Elian Doran
5f14861682 feat(save_indicator): indicate errors 2026-01-02 23:22:33 +02:00
Adorian Doran
8f8493f3ec client/note split: allow enabling background effects according to the MIME type 2026-01-02 23:06:08 +02:00
Elian Doran
62af66b5ae feat(save_indicator): report saving and saved states 2026-01-02 22:53:18 +02:00
Elian Doran
e8d1fa7447 chore(save_indicator): basic infrastructure to display state 2026-01-02 22:44:29 +02:00
Adorian Doran
ee03871405 style/pdf viewer: remove irrelevant elements 2026-01-02 22:28:33 +02:00
Elian Doran
345378d97f feat(save_indicator): add tooltip for each of the states 2026-01-02 22:14:58 +02:00
Elian Doran
07a463ee52 feat(save_indicator): improve display of some states 2026-01-02 22:10:41 +02:00
Elian Doran
3157047160 chore(save_indicator): add opacity 2026-01-02 22:00:25 +02:00
Elian Doran
a1dda3b578 chore(save_indicator): prepare icon and title 2026-01-02 21:58:13 +02:00
Elian Doran
e161ffce57 fix(client/pdf): not always focusing on click 2026-01-02 21:20:29 +02:00
Adorian Doran
0c1859dc43 style/note splits: highlight the active split only in a multi-split view 2026-01-02 20:59:51 +02:00
Elian Doran
e4dcc0f768 chore(client): fix typecheck issues 2026-01-02 20:45:28 +02:00
Elian Doran
74ab591214 chore(package): automatically build share theme & PDF viewer 2026-01-02 20:38:18 +02:00
Elian Doran
7bd7996893 feat(revisions): use customized PDF viewer 2026-01-02 20:17:27 +02:00
Elian Doran
505ae4eeb5 chore(revisions): remove "Preview" heading 2026-01-02 20:02:39 +02:00
Elian Doran
951d6d3ce3 feat(revisions): display PDF preview for revisions 2026-01-02 20:02:13 +02:00
Elian Doran
5ff7764699 style(revisions): prevent revision list from overflowing 2026-01-02 19:49:20 +02:00
Elian Doran
0d74998625 style(revisions): prevent buttons from overflowing 2026-01-02 19:47:07 +02:00
Elian Doran
29b70a12bd feat(revisions): display video preview for revisions 2026-01-02 19:44:23 +02:00
Elian Doran
d84150e97b feat(revisions): display audio preview for revisions 2026-01-02 19:21:51 +02:00
Elian Doran
2b2ef4251f style(revisions): minor spacing adjustments to file table 2026-01-02 18:44:06 +02:00
Elian Doran
2840ea0f38 chore(revisions): display a message when a preview is not available 2026-01-02 18:42:05 +02:00
Elian Doran
542d485267 fix(revisions): missing meta information about revisions 2026-01-02 18:34:45 +02:00
Elian Doran
cdd4fbc81d refactor(client): fix lint warnings in revisions modal 2026-01-02 18:23:04 +02:00
Elian Doran
bfdddab0a0 refactor(client): format revisions dialog 2026-01-02 18:20:55 +02:00
Elian Doran
44d1d01105 fix(pdfjs): preferences don't account for ntxId or noteId 2026-01-02 18:08:25 +02:00
Elian Doran
120bb09171 fix(pdfjs): saving doesn't account for ntxId or noteId 2026-01-02 17:57:43 +02:00
Elian Doran
b7af99c671 refactor(pdfjs): add type safety for messages 2026-01-02 17:57:28 +02:00
Elian Doran
869e0b3973 docs(user): mention updates to the new PDF functions 2026-01-02 12:49:30 +02:00
Elian Doran
b68613dee4 feat(share): integrate custom pdf.js viewer 2026-01-02 12:13:31 +02:00
Elian Doran
ce0f32e7d5 chore(client/pdfjs): remove open file 2026-01-02 11:44:33 +02:00
Elian Doran
78bc9b59c2 chore(client/pdfjs): remove download button from toolbar 2026-01-02 11:41:58 +02:00
Elian Doran
23cf3d2923 feat(client/pdfjs): rewrite download button 2026-01-02 11:40:32 +02:00
Elian Doran
335136f3a3 fix(deps): update dependency preact-render-to-string to v6.6.5 (#8240) 2026-01-02 11:13:42 +02:00
renovate[bot]
11dd7aef09 fix(deps): update dependency preact-render-to-string to v6.6.5 2026-01-02 09:09:55 +00:00
Elian Doran
2d1769e2f9 fix: toggling right pane visibility incorrectly affects all windows (#8226) 2026-01-02 11:08:27 +02:00
Elian Doran
21e26147b0 fix(deps): update dependency react-i18next to v16.5.1 (#8241) 2026-01-02 11:06:38 +02:00
Elian Doran
ba301f8c12 fix(deps): update dependency globals to v17 (#8242) 2026-01-02 11:06:13 +02:00
SiriusXT
3420374649 fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 11:31:59 +08:00
SiriusXT
644d3a181f fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 11:08:49 +08:00
SiriusXT
4be3011a8a fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 10:30:15 +08:00
SiriusXT
5aa0a956dd fix: toggling right pane visibility incorrectly affects all windows 2026-01-02 10:25:34 +08:00
renovate[bot]
7fdb1bdce8 fix(deps): update dependency globals to v17 2026-01-02 01:53:43 +00:00
renovate[bot]
57c6cef2bd fix(deps): update dependency react-i18next to v16.5.1 2026-01-02 01:52:49 +00:00
Elian Doran
e5599adca1 feat(share): Add support for shareJs in static website export (#8173) 2026-01-02 00:39:36 +02:00
Elian Doran
ab392ffb7f Translations update from Hosted Weblate (#8239) 2026-01-02 00:38:23 +02:00
Hosted Weblate
7585d4b258 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-01-01 22:37:01 +00:00
Elian Doran
ff82d9c38c Clean up Vite (#8238) 2026-01-02 00:36:46 +02:00
Elian Doran
920fde69bb chore: add missing space from imports 2026-01-02 00:21:50 +02:00
Elian Doran
053812e5f0 e2e(server): wrong import 2026-01-02 00:14:07 +02:00
Elian Doran
c2f59c4b6c test(server): type error 2026-01-02 00:07:04 +02:00
Elian Doran
06980fe9b5 chore(tsconfig): fix empty type 2026-01-02 00:04:52 +02:00
Elian Doran
3f5616f1fc chore(vitest): fix node:test import 2026-01-02 00:03:45 +02:00
Elian Doran
b6af3b70b0 test(client): increase a timeout for local run 2026-01-01 23:57:43 +02:00
Elian Doran
d8e4547988 chore(vitest): get rid of warning about number of projects 2026-01-01 23:56:26 +02:00
Elian Doran
34f649155e chore(vite): remove vite/global for other projects 2026-01-01 23:44:17 +02:00
Elian Doran
11779fe3e3 chore(vite): remove vite/global for commons 2026-01-01 23:43:59 +02:00
Elian Doran
032cde67b0 chore(vite): remove vite/global for express-partial-content 2026-01-01 23:42:02 +02:00
Elian Doran
229636a796 chore(vite): remove vite/global for server 2026-01-01 23:39:53 +02:00
Elian Doran
da9c9ac346 chore(vite): remove vite/importMeta from spec types 2026-01-01 23:34:39 +02:00
Elian Doran
3fecc4c648 chore(vite): remove vite/client from spec types 2026-01-01 23:33:21 +02:00
Elian Doran
98cefcf77b fix(desktop/pdfjs): not working due to build script 2026-01-01 23:13:21 +02:00
Elian Doran
413ee81ffa Translations update from Hosted Weblate (#8234) 2026-01-01 22:51:03 +02:00
Yatrik Patel
578ca8785e Translated using Weblate (Hindi)
Currently translated at 0.5% (10 of 1740 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-01 20:49:31 +00:00
Yatrik Patel
da4112c078 Translated using Weblate (Hindi)
Currently translated at 2.0% (8 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-01 20:49:30 +00:00
Yatrik Patel
704c7c881d Translated using Weblate (Hindi)
Currently translated at 4.6% (7 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-01 20:49:29 +00:00
Elian Doran
63b6abdb9d PDF.js sidebar experiments for new layout (#8212) 2026-01-01 22:49:15 +02:00
Elian Doran
2e936a3d5c test(pdfjs): disable for now as there are no tests 2026-01-01 22:31:49 +02:00
Elian Doran
606574e18e chore(pdjs): address self-review 2026-01-01 22:15:40 +02:00
Elian Doran
1021879167 chore(client/pdfjs): add some missing translations 2026-01-01 22:15:31 +02:00
Elian Doran
dc4aa9c607 feat(ui): implement tooltips for share icons and clone icons (#8211) 2026-01-01 21:13:01 +02:00
Elian Doran
b2c3d78773 Fix excessive noteContext calls (#8233) 2026-01-01 21:09:39 +02:00
Elian Doran
8d3a0b5295 test(pdfjs): replace beforeAll with beforeEach 2026-01-01 21:06:03 +02:00
lzinga
9879d07bec fix(widget): remove redundant note context update in useLegacyWidget 2026-01-01 11:05:30 -08:00
Elian Doran
7bfce851e7 fix(mermaid) diagrams not saving content and SVG attachment (#8220) 2026-01-01 20:58:56 +02:00
Elian Doran
34e81881ec fix(popupEditor): fix closing of popupEditor when inserting note link (#8224) 2026-01-01 20:56:04 +02:00
Lucas
0143d6c60d Merge branch 'TriliumNext:main' into fix/layout-calls 2026-01-01 10:55:25 -08:00
Elian Doran
267c2bc907 Translations update from Hosted Weblate (#8231) 2026-01-01 19:25:36 +02:00
Yatrik Patel
316f27d88c Translated using Weblate (Hindi)
Currently translated at 0.1% (2 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/hi/
2026-01-01 17:39:40 +01:00
Yatrik Patel
452b56f470 Translated using Weblate (Hindi)
Currently translated at 2.6% (4 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/hi/
2026-01-01 17:39:40 +01:00
Jan Klass
43aeaa4455 Translated using Weblate (German)
Currently translated at 96.1% (1669 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-01-01 17:39:40 +01:00
Yatrik Patel
08b7a6985e Translated using Weblate (Hindi)
Currently translated at 1.0% (4 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/hi/
2026-01-01 17:39:40 +01:00
dirlligafu
4bbd8e28c1 Translated using Weblate (French)
Currently translated at 95.3% (1656 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/fr/
2026-01-01 17:39:40 +01:00
Yatrik Patel
fcf4c09389 Translated using Weblate (Hindi)
Currently translated at 1.7% (2 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/hi/
2026-01-01 17:39:40 +01:00
Elian Doran
2c323cbe80 fix(client): color with uppercase causing exception (closes #8232) 2026-01-01 18:39:31 +02:00
Elian Doran
7ec7b6bd7b chore(pdfjs): manage requested changes 2026-01-01 12:36:33 +02:00
Elian Doran
b2378f2a53 test(server/pdf): move beforeAll 2026-01-01 12:19:51 +02:00
Elian Doran
8bf8d85bb7 test(server/pdf): switching to another note 2026-01-01 11:23:24 +02:00
Elian Doran
676173e895 test(server/pdf): layer listing works 2026-01-01 11:06:29 +02:00
Elian Doran
d8649c87e0 test(server/pdf): attachment listing works 2026-01-01 01:06:26 +02:00
Elian Doran
b9456ca466 test(server/pdf): basic page navigation test 2026-01-01 00:35:39 +02:00
Elian Doran
cfccbb8927 test(server/pdf): basic table of contents test 2025-12-31 23:40:32 +02:00
Elian Doran
a18578362a Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-31 22:55:37 +02:00
Elian Doran
2f9f94dee0 fix(server): pdfjs not available in dist 2025-12-31 22:46:55 +02:00
Elian Doran
c84e45ddee test(pdfjs): set up basic vitest 2025-12-31 21:18:27 +02:00
Lucas
ea558d8c9d Merge branch 'TriliumNext:main' into fix/layout-calls 2025-12-31 07:33:11 -08:00
lzinga
b936a35b63 fix(widget): prevent unnecessary refresh by checking note context change 2025-12-31 07:31:22 -08:00
Elian Doran
b4ef4c2143 chore(pdfjs): fix code scanning issues 2025-12-31 17:27:58 +02:00
Elian Doran
0ff4756ef4 chore(pdfjs): fix typecheck issues 2025-12-31 17:00:56 +02:00
Elian Doran
94204b4739 style(pdf_pages): slight improvement to page layout 2025-12-31 16:45:11 +02:00
Elian Doran
bf3a2b768e chore(pdfjs): set proper target origin when posting messages 2025-12-31 16:37:51 +02:00
SiriusXT
5fb7badfb4 fix(rightPane): toggling right pane visibility incorrectly affects all windows 2025-12-31 19:54:31 +08:00
Elian Doran
239d56f9a3 fix(deps): update dependency @codemirror/view to v6.39.8 (#8222) 2025-12-31 10:38:22 +02:00
Elian Doran
9163fc23f4 chore(deps): update dependency @redocly/cli to v2.14.2 (#8221) 2025-12-31 10:37:30 +02:00
Elian Doran
d225c28fde chore(deps): update pnpm to v10.27.0 (#8223) 2025-12-31 10:29:28 +02:00
SiriusXT
8a3f02e845 fix(popupEditor): fix closing of popupEditor when inserting note link 2025-12-31 14:12:38 +08:00
renovate[bot]
d0dc92c891 chore(deps): update pnpm to v10.27.0 2025-12-31 02:34:03 +00:00
renovate[bot]
8d660f5a2f fix(deps): update dependency @codemirror/view to v6.39.8 2025-12-31 02:33:52 +00:00
renovate[bot]
b41b4e77b2 chore(deps): update dependency @redocly/cli to v2.14.2 2025-12-31 02:33:14 +00:00
lzinga
267a37d3bd feat(component): add removeChild method for cleanup of child components
feat(hooks): improve useLegacyWidget cleanup and memoization logic
2025-12-30 13:59:46 -08:00
Lucas
0cf23c7d7c Update apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-30 11:56:34 -08:00
lzinga
a632486229 fix(mermaid) diagrams not saving content and SVG attachment 2025-12-30 11:35:48 -08:00
Elian Doran
64a518a00b chore(deps): update typescript-eslint monorepo to v8.51.0 (#8214) 2025-12-30 12:07:14 +02:00
Elian Doran
2f3a914027 Translations update from Hosted Weblate (#8213) 2025-12-30 12:06:27 +02:00
Elian Doran
7182d32d9c Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-30 11:47:00 +02:00
green
18381c5d32 Translated using Weblate (Japanese)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2025-12-30 09:00:07 +01:00
Kuzma Simonov
79327073b4 Translated using Weblate (Russian)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2025-12-30 09:00:06 +01:00
Giovi
018f2fd789 Translated using Weblate (Italian)
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2025-12-30 09:00:06 +01:00
noobhjy
3889392aed Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1736 of 1736 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-12-30 09:00:05 +01:00
Elian Doran
bd976a25f1 style(next): use border for focus instead 2025-12-30 09:59:46 +02:00
Elian Doran
01f05ac6fd fix(pdf): active context not changed when clicking preview 2025-12-30 09:47:02 +02:00
Elian Doran
52292cb5a5 style(next): indicate active note context 2025-12-30 09:31:03 +02:00
perfectra1n
c6dd1ba0ca fix(types): resolve typecheck issue with note_tree 2025-12-29 18:25:55 -08:00
renovate[bot]
84f069087c chore(deps): update typescript-eslint monorepo to v8.51.0 2025-12-30 01:05:53 +00:00
Elian Doran
76cfced60f chore(pdfjs): fix partially removed method 2025-12-30 01:43:07 +02:00
Elian Doran
5c2aea0a6b chore: remove LLM generated doc 2025-12-30 01:40:34 +02:00
Elian Doran
7a883c62df Merge remote-tracking branch 'origin/main' into feature/pdfjs_sidebar_experiments 2025-12-30 01:40:03 +02:00
Elian Doran
ff97461ff8 PDF.js integration (part I) (#8206) 2025-12-30 01:39:09 +02:00
Elian Doran
51b0eb74a5 chore(pdfjs): address requested changes 2025-12-30 01:37:44 +02:00
Elian Doran
2304407986 chore(pdfjs): address origin concerns 2025-12-30 01:27:05 +02:00
Elian Doran
a1ebdc3004 chore(pdfjs): integrate into typecheck 2025-12-30 01:23:19 +02:00
Elian Doran
fef30f4bea chore(client): fix typecheck 2025-12-30 01:20:29 +02:00
Elian Doran
eee8d9ab7c feat(pdfjs): optionally hide left sidebar 2025-12-30 01:09:45 +02:00
perfectra1n
4f2678d321 feat(ui): implement tooltips for share icons and clone icons
asdf
2025-12-29 14:42:34 -08:00
Elian Doran
c473fba628 refactor(right_pane): move PDF-specific components in own dir 2025-12-30 00:17:29 +02:00
Elian Doran
9a9cd8e6a5 feat(right_pane): add count in title for PDF items 2025-12-30 00:16:45 +02:00
Elian Doran
f5a89aa81a feat(right_pane): hide PDF attachments/layers when not needed 2025-12-30 00:10:23 +02:00
Elian Doran
3c1beab725 fix(pdf_pages): fix a few type errors 2025-12-30 00:00:03 +02:00
Elian Doran
79f03ad3ac fix(pdf_layers): toggling layers and updating state not working 2025-12-29 23:57:52 +02:00
Elian Doran
574138a1fb refactor(pdf_layers): get layers to show 2025-12-29 23:08:35 +02:00
Elian Doran
6513e2cfca refactor(pdf_attachments): deduplicate font size 2025-12-29 23:04:02 +02:00
Elian Doran
43a749b6a7 feat(right_pane): display attachments 2025-12-29 22:56:06 +02:00
Elian Doran
c1d6b3121a fix(pdf_pages): pages not updating between notes 2025-12-29 22:50:48 +02:00
Elian Doran
0d9c8ae4df style(pdf_pages): page numbers within pages 2025-12-29 22:46:20 +02:00
Elian Doran
62d8c089ed chore(pdf_pages): remove logs 2025-12-29 22:43:17 +02:00
Elian Doran
971a76ce11 style(pdf_pages): render in multiple columns 2025-12-29 22:39:38 +02:00
Elian Doran
cb33404122 feat(client/right_pane): use intersection observer for performance 2025-12-29 22:36:03 +02:00
Elian Doran
bcf72f4624 feat(client/right_pane): display pages 2025-12-29 22:34:36 +02:00
Elian Doran
77ad6950e8 feat(client/right_pane): highlight current heading 2025-12-29 22:11:25 +02:00
Elian Doran
e2d29aadca refactor(pdfjs): extract toc logic to separate file 2025-12-29 21:58:11 +02:00
Elian Doran
64ca04ad07 feat(client/right_pane): jump to heading 2025-12-29 21:55:47 +02:00
Elian Doran
b6506a9331 chore(client/right_pane): get table of contents to show 2025-12-29 21:49:02 +02:00
Elian Doran
fd7222242a chore(pdf): process PDF outline 2025-12-29 21:44:49 +02:00
Elian Doran
e36049cd43 chore(client/right_pane): get raw ToC data to show up 2025-12-29 21:44:15 +02:00
Elian Doran
257f6c5994 chore(client/right_pane): inject title into PDF toc sidebar 2025-12-29 21:28:17 +02:00
Elian Doran
9098bfb63a chore(client): prototype implementation to communicate data through note context 2025-12-29 21:26:52 +02:00
Wael Nasreddine
118d22c4ec Merge branch 'main' into static-implement-sharejs 2025-12-29 11:02:07 -08:00
Elian Doran
758df0d85a fix(share): Prevent crashing if candidate note is null (#8164) 2025-12-29 20:43:12 +02:00
Elian Doran
59bbd902fc feat(share): Render JS Frontend files as-is with extension .js (#8172) 2025-12-29 20:41:46 +02:00
Elian Doran
fffab73061 feat(pdfjs): auto-watch dev 2025-12-29 19:23:56 +02:00
Elian Doran
0a9ce84cf2 feat(client/pdf): respect locale 2025-12-29 19:10:14 +02:00
Elian Doran
07a1734d4b chore(pdfjs): copy locales during build 2025-12-29 19:09:03 +02:00
Elian Doran
6e41d3591d chore(pdfjs): add locales 2025-12-29 19:06:08 +02:00
Elian Doran
4134e5054a fix(client/pdf): not refreshing when uploading new revision 2025-12-29 17:02:22 +02:00
Elian Doran
bb374a5ce2 fix(client/pdf): blob reloaded when saving 2025-12-29 16:46:30 +02:00
Elian Doran
359f398afa feat(pdfjs): debounce saving view config 2025-12-29 16:19:21 +02:00
Elian Doran
84425e86e9 feat(client/pdf): filter out view config by fingerprint 2025-12-29 16:15:38 +02:00
Elian Doran
ebf725c949 feat(client/pdf): store and restore page position 2025-12-29 15:55:47 +02:00
Elian Doran
fc0ea36cf3 chore(pdfjs): first attempt at intercepting store 2025-12-29 14:06:59 +02:00
Elian Doran
7836de3f08 fix(client/pdf): form elements not detected for save 2025-12-29 13:29:04 +02:00
Elian Doran
406232c478 chore(pdfjs): log event bus 2025-12-29 13:23:54 +02:00
Elian Doran
9e0c29496f refactor(pdfjs): use TypeScript for the custom script 2025-12-29 13:14:00 +02:00
Elian Doran
480954ee87 feat(pdfjs): react to dark mode 2025-12-29 12:51:43 +02:00
Elian Doran
94039bd9b1 chore(pdfjs): improve toolbar contrast 2025-12-29 12:40:43 +02:00
Elian Doran
667eaca9f2 feat(pdfjs): improve style to better match Trilium 2025-12-29 12:35:49 +02:00
Elian Doran
446822a7ae chore(client/pdf): inject some CSS variables 2025-12-29 12:21:44 +02:00
Elian Doran
f09a3e06f4 refactor(client/pdf): split into own component 2025-12-29 12:01:47 +02:00
Elian Doran
7c4a56f5f2 chore(deps): add missing pdfjs dependency 2025-12-29 11:10:12 +02:00
Elian Doran
08f6a32c34 fix(client/pdfjs): not reacting to all changes 2025-12-29 10:33:57 +02:00
Elian Doran
3e255fa647 feat(client/pdf): add debouncing 2025-12-29 10:15:15 +02:00
Elian Doran
c0a90402ef feat(client/pdf): save annotations by uploading new revision 2025-12-29 09:51:54 +02:00
Elian Doran
5e42627bce chore(client/pdf): basic reaction to annotations 2025-12-29 02:00:59 +02:00
Elian Doran
41bcf9524a feat(client/pdf): integrate pdf.js 2025-12-29 01:16:56 +02:00
Elian Doran
914cf10911 chore(pdfjs): get icons to show up 2025-12-29 01:11:04 +02:00
Elian Doran
855d4d139d chore(pdfjs): get to actually render something 2025-12-29 01:03:16 +02:00
Elian Doran
abb7b0f8c8 feat(server): serve pdfjs over static route 2025-12-29 00:50:59 +02:00
Elian Doran
d78ad52662 chore(pdfjs): copy viewer to dist 2025-12-29 00:45:57 +02:00
Elian Doran
25b4bcd311 chore(pdfjs): create empty package 2025-12-29 00:26:37 +02:00
Wael Nasreddine
1d3e971ed7 Merge branch 'static-correct-type' into static-implement-sharejs
* static-correct-type:
  improve the protected note handling
  be loosy and honor startsWith application/javascript
2025-12-25 23:01:30 -08:00
Wael Nasreddine
3d1f6c4f91 be loosy and honor startsWith application/javascript 2025-12-25 22:54:14 -08:00
Wael Nasreddine
8368969932 implement the second part of the sharejs 2025-12-25 22:06:30 -08:00
Wael Nasreddine
cb016c4307 Address Gemini's comment 2025-12-25 16:26:58 -08:00
Wael Nasreddine
7c7797d35a fix(share/prev_next): Prevent crashing if candide page is null
When a note is not visible, attempting to export it ends up crashing the
server with this error:

```
TypeError: ejs:193
    191|
    192|                 <% if (hasTree) { %>
 >> 193|                     <%- include("prev_next", { note: note, subRoot: subRoot }) %>
    194|                 <% } %>
    195|             </footer>
    196|         </div>
ejs:1
 >> 1| <%
    2|     // TODO: code cleanup + putting this behind a toggle/attribute
    3|     const previousNote = (() => {
    4|         // If we are at the subRoot, there is no previous
Cannot read properties of undefined (reading 'hasVisibleChildren')
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:27:26)
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:34:7)
    at d (/usr/src/app/main.cjs:557:265)
    at g (/usr/src/app/main.cjs:557:251)
    at eval (eval at compile (/usr/src/app/main.cjs:553:203), <anonymous>:293:17)
    at d (/usr/src/app/main.cjs:557:265)
    at as.render (/usr/src/app/main.cjs:532:458)
    at Omr (/usr/src/app/main.cjs:581:109552)
    at Rmr (/usr/src/app/main.cjs:581:107637)
    at $W.prepareContent (/usr/src/app/main.cjs:653:28) {
  path: ''
```

fixes #8002
fixes #8162
2025-12-25 16:11:01 -08:00
meinzzzz
87ab41c80c Fix shift+tab behavior in MathInputView 2025-12-23 18:02:40 +01:00
Meinzzzz
d2391f94c0 Fix offline math rendering by bundling local fonts 2025-12-15 21:32:50 +01:00
Meinzzzz
050ddb8c55 Improve css to fix tooltips 2025-12-15 20:17:58 +01:00
Meinzzzz
bc23e0984a Undo unnecessary formatting changes 2025-12-14 22:00:56 +01:00
Meinzzzz
07de353207 Adding comments and improving code quality in math input views 2025-12-14 20:21:42 +01:00
Meinzzzz
c02491d2e6 Remove unnecessary any casts in math plugin 2025-12-12 23:09:20 +01:00
Meinzzzz
a6ede8f905 Improve mathinputview 2025-12-12 21:33:59 +01:00
Meinzzzz
22941a9ce0 Fix sync issues 2025-12-12 19:48:09 +01:00
Meinzzzz
633a09d414 Fix sync bug 2025-12-11 23:06:13 +01:00
Meinzzzz
29f0881c5a Fix clicking issue in Mathfield 2025-12-10 22:44:02 +01:00
Meinzzzz
60debca37b Improve comments 2025-12-10 18:36:34 +01:00
Meinzzzz
30ea81d0fb Improve virtual keyboard logic and fix Tab issues 2025-12-08 22:59:08 +01:00
Meinzzzz
b1d92c4fe6 Fix Tab issues 2025-12-08 22:39:12 +01:00
Meinzzzz
70f46de2d8 MathLive virtual keyboard only appears when focusing the mathfield 2025-12-08 20:30:07 +01:00
Meinzzzz
f1b2d0b870 Increas Mathfield font size and ensure virtual keyboard appears above CKEditor 2025-12-08 20:22:52 +01:00
Meinzzzz
8a385972fc Close Virtual Keyboard when Mathinput is closed 2025-12-08 18:49:06 +01:00
Meinzzzz
28dd85c1d1 Merge upstream changes and resolve conflicts 2025-12-07 23:51:41 +01:00
meinzzzz
827c8e0e72 Refactor: Combine MathLive and LaTeX inputs into one single component 2025-12-07 23:19:48 +01:00
meinzzzz
162c076a14 Improve MathLive integration and lazy loading 2025-12-02 22:30:37 +01:00
meinzzzz
9386465de7 Added mathrender error class for better error handling in math rendering 2025-12-02 22:29:20 +01:00
meinzzzz
acca22f3a1 Improve Synchronization Between Mathlive and rawlatex input 2025-12-02 22:28:16 +01:00
meinzzzz
f8d84814e0 Fix differential d problems 2025-11-26 23:02:34 +01:00
meinzzzz
c46cf41842 Small improvements 2025-11-26 22:48:57 +01:00
meinzzzz
64ab1c4116 Imrovement for Latex 2025-11-26 22:29:29 +01:00
meinzzzz
a6de1041c7 Fix bug in math rendering where old content was not cleared 2025-11-26 21:59:33 +01:00
meinzzzz
c8d34e65ea Improve max window size 2025-11-26 21:49:09 +01:00
meinzzzz
51db729546 Improve and simplify Mathfield integration 2025-11-25 23:27:06 +01:00
meinzzzz
d2052ad236 Disable mathlive sound effects 2025-11-24 21:51:59 +01:00
meinzzzz
9c4301467f Remove unused icons from ckeditor5-math package 2025-11-24 19:46:04 +01:00
meinzzzz
e7355dc0e4 remove gitignore unneccesary changes 2025-11-24 18:43:52 +01:00
meinzzzz
4110fec94f Removed unnecessary declare keyboard 2025-11-24 18:28:59 +01:00
meinzzzz
d5e601eae9 Simpliyfied resize logic for math input form and improved css 2025-11-24 17:56:18 +01:00
meinzzzz
4f044c4a57 Use icons form CKEditor5 icons, instead of testing icons. 2025-11-23 22:43:07 +01:00
meinzzzz
5821c350e1 Fixing class property initialization order 2025-11-23 17:58:51 +01:00
meinzzzz
edba8188fe Fix dark selection colors in MathLive math-field 2025-11-23 13:44:28 +01:00
meinzzzz
1471a72633 refactor: avoid recursive updates in mathLiveInput by normalizing value before updateing 2025-11-23 13:34:22 +01:00
meinzzzz
56834cb88a Improve MathLive and Raw LaTeX input views to propagate mousedown events 2025-11-23 13:29:26 +01:00
meinzzzz
a0f16f9184 Fix typos in mathform.css 2025-11-23 13:09:56 +01:00
meinzzzz
de80eb4806 Improve mathform.css styling for better visual integration 2025-11-22 22:42:34 +01:00
meinzzzz
48a4b81fbe remove automated screenshot files 2025-11-22 21:40:55 +01:00
meinzzzz
e225794f72 Better window focus handling in MathFormView 2025-11-22 21:35:37 +01:00
meinzzzz
4eef30f8b5 Fix names 2025-11-22 00:20:20 +01:00
meinzzzz
569b09609d Remove mathlive dependency and chunking 2025-11-22 00:01:14 +01:00
meinzzzz
39838c25c2 Fixed chaching problems 2025-11-21 23:50:49 +01:00
meinzzzz
49e90c08a9 Better Names for Math UI Components 2025-11-20 22:45:21 +01:00
meinzzzz
e777b06fb8 Math 2025-11-20 18:53:39 +01:00
meinzzzz
497ec2ac74 Merge branch 'main' of https://github.com/Meinzzzz/Trilium-Mathlive 2025-11-20 18:00:18 +01:00
meinzzzz
c5d282d203 Mathlive 2025-11-20 00:09:10 +01:00
641 changed files with 90461 additions and 6931 deletions

30
.github/workflows/i18n.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Internationalization
on:
push:
branches:
- "weblate:*"
workflow_dispatch:
pull_request:
paths:
- "apps/client/src/translations/**"
- ".github/workflows/i18n.yml"
permissions:
contents: read
jobs:
i18n-check:
name: Check i18n translations
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
- name: Set up node & dependencies
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check translations
run: pnpm tsx scripts/translation/check-translation-coverage.ts

View File

@@ -11,6 +11,14 @@ concurrency:
cancel-in-progress: true
jobs:
sanity-check:
name: Sanity Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Check version consistency
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
make-electron:
name: Make Electron
strategy:

View File

@@ -34,7 +34,7 @@ jobs:
cache: "pnpm"
- name: Install dependencies
run: pnpm install --filter website --frozen-lockfile
run: pnpm install --filter website --frozen-lockfile --ignore-scripts
- name: Build the website
run: pnpm website:build

1
.gitignore vendored
View File

@@ -51,3 +51,4 @@ upload
# docs
site/
apps/*/coverage
scripts/translation/.language*.json

2
.nvmrc
View File

@@ -1 +1 @@
24.12.0
24.13.0

View File

@@ -165,6 +165,17 @@ pnpm install
pnpm edit-docs:edit-docs
```
Alternatively, if you have Nix installed:
```shell
# Run directly
nix run .#edit-docs
# Or install to your profile
nix profile install .#edit-docs
trilium-edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell

View File

@@ -9,14 +9,14 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.26.2",
"packageManager": "pnpm@10.28.0",
"devDependencies": {
"@redocly/cli": "2.14.1",
"@redocly/cli": "2.14.5",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"typedoc": "0.28.15",
"typedoc": "0.28.16",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.101.1",
"version": "0.101.3",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -43,8 +43,8 @@
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "16.5.0",
"i18next": "25.7.3",
"globals": "17.0.0",
"i18next": "25.7.4",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery.fancytree": "2.38.5",
@@ -56,12 +56,12 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.3.8",
"mind-elixir": "5.5.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.1",
"react-i18next": "16.5.0",
"react-window": "2.2.3",
"preact": "10.28.2",
"react-i18next": "16.5.2",
"react-window": "2.2.5",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -69,7 +69,7 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@prefresh/vite": "2.4.11",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@types/leaflet": "1.9.21",
@@ -78,7 +78,8 @@
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.0.11",
"happy-dom": "20.1.0",
"lightningcss": "1.30.2",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.1.4"
}

View File

@@ -6,7 +6,7 @@ import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import froca from "../services/froca.js";
import { initLocale,t } from "../services/i18n.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
import linkService, { type ViewScope } from "../services/link.js";
import type LoadResults from "../services/load_results.js";
@@ -154,6 +154,7 @@ export type CommandMappings = {
};
openInTab: ContextMenuCommandData;
openNoteInSplit: ContextMenuCommandData;
openNoteInWindow: ContextMenuCommandData;
openNoteInPopup: ContextMenuCommandData;
toggleNoteHoisting: ContextMenuCommandData;
insertNoteAfter: ContextMenuCommandData;
@@ -382,7 +383,8 @@ export type CommandMappings = {
reloadTextEditor: CommandData;
chooseNoteType: CommandData & {
callback: ChooseNoteTypeCallback
}
};
customDownload: CommandData;
};
type EventMappings = {
@@ -473,6 +475,11 @@ type EventMappings = {
noteContextRemoved: {
ntxIds: string[];
};
contextDataChanged: {
noteContext: NoteContext;
key: string;
value: unknown;
};
exportSvg: { ntxId: string | null | undefined; };
exportPng: { ntxId: string | null | undefined; };
geoMapCreateChildNote: {

View File

@@ -57,6 +57,18 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
return this;
}
/**
* Removes a child component from this component's children array.
* This is used for cleanup when a widget is unmounted to prevent event listener accumulation.
*/
removeChild(component: ChildT) {
const index = this.children.indexOf(component);
if (index !== -1) {
this.children.splice(index, 1);
component.parent = undefined;
}
}
handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
try {
const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);

View File

@@ -12,6 +12,7 @@ import server from "../services/server.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
import type { HeadingContext } from "../widgets/sidebar/TableOfContents.js";
import appContext, { type EventData, type EventListener } from "./app_context.js";
import Component from "./component.js";
@@ -22,6 +23,31 @@ export interface SetNoteOpts {
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
export type SaveState = "saved" | "saving" | "unsaved" | "error";
export interface NoteContextDataMap {
toc: HeadingContext;
pdfPages: {
totalPages: number;
currentPage: number;
scrollToPage(page: number): void;
requestThumbnail(page: number): void;
};
pdfAttachments: {
attachments: PdfAttachment[];
downloadAttachment(filename: string): void;
};
pdfLayers: {
layers: PdfLayer[];
toggleLayer(layerId: string, visible: boolean): void;
};
saveState: {
state: SaveState;
};
}
type ContextDataKey = keyof NoteContextDataMap;
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null;
hoistedNoteId: string;
@@ -32,6 +58,13 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
parentNoteId?: string | null;
viewScope?: ViewScope;
/**
* Metadata storage for UI components (e.g., table of contents, PDF page list, code outline).
* This allows type widgets to publish data that sidebar/toolbar components can consume.
* Data is automatically cleared when navigating to a different note.
*/
private contextData: Map<string, unknown> = new Map();
constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) {
super();
@@ -91,6 +124,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
this.viewScope = opts.viewScope;
({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath));
// Clear context data when switching notes and notify subscribers
const oldKeys = Array.from(this.contextData.keys());
this.contextData.clear();
if (oldKeys.length > 0) {
// Notify subscribers asynchronously to avoid blocking navigation
window.setTimeout(() => {
for (const key of oldKeys) {
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
}, 0);
}
this.saveToRecentNotes(resolvedNotePath);
protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
@@ -443,6 +492,52 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return title;
}
/**
* Set metadata for this note context (e.g., table of contents, PDF pages, code outline).
* This data can be consumed by sidebar/toolbar components.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages", "codeOutline")
* @param value - The data to store (will be cleared when switching notes)
*/
setContextData<K extends ContextDataKey>(key: K, value: NoteContextDataMap[K]): void {
this.contextData.set(key, value);
// Trigger event so subscribers can react
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value
});
}
/**
* Get metadata for this note context.
*
* @param key - The data key to retrieve
* @returns The stored data, or undefined if not found
*/
getContextData<K extends ContextDataKey>(key: K): NoteContextDataMap[K] | undefined {
return this.contextData.get(key) as NoteContextDataMap[K] | undefined;
}
/**
* Check if context data exists for a given key.
*/
hasContextData(key: ContextDataKey): boolean {
return this.contextData.has(key);
}
/**
* Clear specific context data.
*/
clearContextData(key: ContextDataKey): void {
this.contextData.delete(key);
this.triggerEvent("contextDataChanged", {
noteContext: this,
key,
value: undefined
});
}
}
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {

View File

@@ -8,7 +8,7 @@ import search from "../services/search.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import type FAttachment from "./fattachment.js";
import type { AttributeType,default as FAttribute } from "./fattribute.js";
import type { AttributeType, default as FAttribute } from "./fattribute.js";
const LABEL = "label";
const RELATION = "relation";
@@ -616,7 +616,9 @@ export default class FNote {
}
isFolder() {
return this.type === "search" || this.getFilteredChildBranches().length > 0;
if (this.isLabelTruthy("subtreeHidden")) return false;
if (this.type === "search") return true;
return this.getFilteredChildBranches().length > 0;
}
getFilteredChildBranches() {

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<title>Trilium Notes</title>
</head>
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required to match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<script src="./index.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>
if (typeof module === 'object') {window.module = module; module = undefined;}
</script>
</body>
</html>

View File

@@ -0,0 +1,110 @@
async function bootstrap() {
showSplash();
await setupGlob();
await Promise.all([
initJQuery(),
loadBootstrapCss()
]);
loadStylesheets();
loadIcons();
setBodyAttributes();
await loadScripts();
hideSplash();
}
async function initJQuery() {
const $ = (await import("jquery")).default;
window.$ = $;
window.jQuery = $;
}
async function setupGlob() {
const response = await fetch(`/bootstrap${window.location.search}`);
const json = await response.json();
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
};
}
async function loadBootstrapCss() {
// We have to selectively import Bootstrap CSS based on text direction.
if (glob.isRtl) {
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
} else {
await import("bootstrap/dist/css/bootstrap.min.css");
}
}
function loadStylesheets() {
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: string[] = [];
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
document.head.appendChild(linkEl);
}
}
function loadIcons() {
const styleEl = document.createElement("style");
styleEl.innerText = window.glob.iconPackCss;
document.head.appendChild(styleEl);
}
function setBodyAttributes() {
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
`heading-style-${headingStyle}`,
`layout-${layoutOrientation}`,
`platform-${platform}`,
isElectron && "electron",
hasNativeTitleBar && "native-titlebar",
hasBackgroundEffects && "background-effects"
].filter(Boolean) as string[];
for (const classToSet of classesToSet) {
document.body.classList.add(classToSet);
}
document.body.lang = currentLocale.id;
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
}
async function loadScripts() {
if (glob.device === "mobile") {
await import("./mobile.js");
} else {
await import("./desktop.js");
}
}
function showSplash() {
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
document.body.style.display = "none";
}
function hideSplash() {
document.body.style.display = "block";
}
bootstrap();

View File

@@ -1,35 +1,35 @@
import { applyModals } from "./layout_commons.js";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import type AppContext from "../components/app_context.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import RootContainer from "../widgets/containers/root_container.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import type AppContext from "../components/app_context.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import { applyModals } from "./layout_commons.js";
const MOBILE_CSS = `
<style>
@@ -194,11 +194,11 @@ export default class MobileLayout {
}
function FilePropertiesWrapper() {
const { note } = useNoteContext();
const { note, ntxId } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} />}
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
</div>
);
}

View File

@@ -1,21 +1,21 @@
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import type FAttachment from "../entities/fattachment.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import clipboard from "../services/clipboard.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import noteCreateService from "../services/note_create.js";
import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
// TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse {
@@ -72,6 +72,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search";
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
const parentNotSearch = !parentNote || parentNote.type !== "search";
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
@@ -79,17 +81,18 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const items: (MenuItem<TreeCommandNames> | null)[] = [
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
isHoisted
? null
: {
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
title: `${t("tree-context-menu.hoist-note")}`,
command: "toggleNoteHoisting",
keyboardShortcut: "toggleNoteHoisting",
uiIcon: "bx bxs-chevrons-up",
enabled: noSelectedNotes && notSearch
},
!isHoisted || !isNotRoot
? null
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
@@ -112,7 +115,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
keyboardShortcut: "createNoteInto",
uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
enabled: notSearch && noSelectedNotes && notOptionsOrHelp && !hasSubtreeHidden && !isSpotlighted,
columns: 2
},
@@ -150,8 +153,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ kind: "separator" },
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
uiIcon: "bx bx-show",
handler: async () => {
const note = await froca.getNote(this.node.data.noteId);
if (!note) return;
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
}
},
{
title: t("tree-context-menu.sort-by"),
command: "sortChildNotes",
@@ -164,7 +176,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
]
].filter(Boolean) as MenuItem<TreeCommandNames>[]
},
{ kind: "separator" },
@@ -292,25 +304,30 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
type,
isProtected,
templateNoteId
});
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
templateNoteId
});
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInWindow") {
appContext.triggerCommand("openInWindow", {
notePath,
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
});
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
@@ -332,11 +349,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText("#" + notePath);
navigator.clipboard.writeText(`#${ notePath}`);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath: notePath,
notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)

View File

@@ -0,0 +1,139 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildNote } from "../test/easy-froca";
import { setBooleanWithInheritance } from "./attributes";
import froca from "./froca";
import server from "./server.js";
// Spy on server methods to track calls
// @ts-expect-error the generic typing is causing issues here
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
// @ts-expect-error the generic typing is causing issues here
server.remove = vi.fn(async <T> (url: string) => ({} as T));
describe("Set boolean with inheritance", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("doesn't call server if value matches directly", async () => {
const noteWithLabel = buildNote({
title: "New note",
"#foo": ""
});
const noteWithoutLabel = buildNote({
title: "New note"
});
await setBooleanWithInheritance(noteWithLabel, "foo", true);
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("sets boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note"
});
await setBooleanWithInheritance(standaloneNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("removes boolean normally without inheritance", async () => {
const standaloneNote = buildNote({
title: "New note",
"#foo": ""
});
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
await setBooleanWithInheritance(standaloneNote, "foo", false);
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
});
it("doesn't call server if value matches inherited", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).not.toHaveBeenCalled();
expect(server.remove).not.toHaveBeenCalled();
});
it("overrides boolean with inheritance", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(true);
await setBooleanWithInheritance(childNote, "foo", false);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "false",
isInheritable: false
});
});
it("overrides boolean with inherited false", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note"
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
it("deletes override boolean with inherited false with already existing value", async () => {
const parentNote = buildNote({
title: "Parent note",
"#foo(inheritable)": "false",
"children": [
{
title: "Child note",
"#foo": "false",
}
]
});
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
expect(childNote.isLabelTruthy("foo")).toBe(false);
await setBooleanWithInheritance(childNote, "foo", true);
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
type: "label",
name: "foo",
value: "",
isInheritable: false
});
});
});

View File

@@ -1,14 +1,15 @@
import server from "./server.js";
import froca from "./froca.js";
import type FNote from "../entities/fnote.js";
import type { AttributeRow } from "./load_results.js";
import { AttributeType } from "@triliumnext/commons";
import type FNote from "../entities/fnote.js";
import froca from "./froca.js";
import type { AttributeRow } from "./load_results.js";
import server from "./server.js";
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/attribute`, {
type: "label",
name: name,
value: value,
name,
value,
isInheritable
});
}
@@ -16,8 +17,8 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/set-attribute`, {
type: "label",
name: name,
value: value,
name,
value,
isInheritable
});
}
@@ -25,12 +26,42 @@ export async function setLabel(noteId: string, name: string, value: string = "",
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
await server.put(`notes/${noteId}/set-attribute`, {
type: "relation",
name: name,
value: value,
name,
value,
isInheritable
});
}
/**
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
* from the inherited value, an owned label will be created or updated to reflect the desired value.
*
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
*
* @param note the note on which to set the boolean label.
* @param labelName the name of the label to set.
* @param value the boolean value to set for the label.
*/
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
const actualValue = note.isLabelTruthy(labelName);
if (actualValue === value) return;
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
if (hasInheritedValue) {
if (value) {
setLabel(note.noteId, labelName, "");
} else {
// Label is inherited - override to false.
setLabel(note.noteId, labelName, "false");
}
} else if (value) {
setLabel(note.noteId, labelName, "");
} else {
removeOwnedLabelByName(note, labelName);
}
}
async function removeAttributeById(noteId: string, attributeId: string) {
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
}
@@ -142,6 +173,7 @@ export default {
setLabel,
setRelation,
setAttribute,
setBooleanWithInheritance,
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,

View File

@@ -1,12 +1,12 @@
import utils from "./utils.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import appContext from "../components/app_context.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
import server from "./server.js";
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
import utils from "./utils.js";
import ws from "./ws.js";
// TODO: Deduplicate type with server
interface Response {
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
}
}
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
const newParentBranch = froca.getBranch(newParentBranchId);
if (!newParentBranch) {
return;
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
continue;
}
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
if (!resp.success) {
toastService.showError(resp.message);

View File

@@ -1,18 +1,19 @@
import renderService from "./render.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import WheelZoom from 'vanilla-js-wheel-zoom';
import FAttachment from "../entities/fattachment.js";
import FNote from "../entities/fnote.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { t } from "../services/i18n.js";
import renderText from "./content_renderer_text.js";
import renderDoc from "./doc_renderer.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import openService from "./open.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import openService from "./open.js";
import utils from "./utils.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import renderService from "./render.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import renderDoc from "./doc_renderer.js";
import { t } from "../services/i18n.js";
import WheelZoom from 'vanilla-js-wheel-zoom';
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import renderText from "./content_renderer_text.js";
import utils from "./utils.js";
let idCounter = 1;
@@ -22,6 +23,12 @@ export interface RenderOptions {
imageHasZoom?: boolean;
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
noChildrenList?: boolean;
/** If enabled, it will prevent rendering of included notes. */
noIncludedNotes?: boolean;
/** If enabled, it will include archived notes when rendering children list. */
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
}
const CODE_MIME_TYPES = new Set(["application/json"]);
@@ -152,7 +159,7 @@ function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLE
const $img = $("<img>")
.attr("src", url || "")
.attr("id", "attachment-image-" + idCounter++)
.attr("id", `attachment-image-${idCounter++}`)
.css("max-width", "100%");
$renderedContent.append($img);
@@ -193,7 +200,7 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open`));
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
$content.append($pdfPreview);
} else if (type === "audio") {
@@ -217,28 +224,28 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
// in attachment list
const $downloadButton = $(`
<button class="file-download btn btn-primary" type="button">
<span class="bx bx-download"></span>
<span class="tn-icon bx bx-download"></span>
${t("file_properties.download")}
</button>
`);
const $openButton = $(`
<button class="file-open btn btn-primary" type="button">
<span class="bx bx-link-external"></span>
<span class="tn-icon bx bx-link-external"></span>
${t("file_properties.open")}
</button>
`);
$downloadButton.on("click", (e) => {
e.stopPropagation();
openService.downloadFileNote(entity.noteId)
openService.downloadFileNote(entity, null, null);
});
$openButton.on("click", async (e) => {
const iconEl = $openButton.find("> .bx");
iconEl.removeClass("bx bx-link-external");
iconEl.addClass("bx bx-loader spin");
e.stopPropagation();
await openService.openNoteExternally(entity.noteId, entity.mime)
await openService.openNoteExternally(entity.noteId, entity.mime);
iconEl.removeClass("bx bx-loader spin");
iconEl.addClass("bx bx-link-external");
});
@@ -266,7 +273,7 @@ async function renderMermaid(note: FNote | FAttachment, $renderedContent: JQuery
try {
await loadElkIfNeeded(mermaid, content);
const { svg } = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
const { svg } = await mermaid.mermaidAPI.render(`in-mermaid-graph-${idCounter++}`, content);
$renderedContent.append($(postprocessMermaidSvg(svg)));
} catch (e) {

View File

@@ -0,0 +1,132 @@
import { trimIndentation } from "@triliumnext/commons";
import { describe, expect, it } from "vitest";
import { buildNote } from "../test/easy-froca";
import renderText from "./content_renderer_text";
describe("Text content renderer", () => {
it("renders included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1);
});
it("skips rendering included note", async () => {
const contentEl = document.createElement("div");
const includedNote = buildNote({
title: "Included note",
content: "<p>This is the included note.</p>"
});
const note = buildNote({
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl), { noIncludedNotes: true });
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on direct recursion", async () => {
const contentEl = document.createElement("div");
const note = buildNote({
title: "New note",
id: "Y7mBwmRjQyb4",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
});
it("doesn't enter infinite loop on indirect recursion", async () => {
const contentEl = document.createElement("div");
buildNote({
id: "first",
title: "Included note",
content: trimIndentation`\
<p>This is the included note.</p>
<section class="include-note" data-note-id="second" data-box-size="medium">
&nbsp;
</section>
`
});
const note = buildNote({
id: "second",
title: "New note",
content: trimIndentation`
<p>
Hi there
</p>
<section class="include-note" data-note-id="first" data-box-size="medium">
&nbsp;
</section>
`
});
await renderText(note, $(contentEl));
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
});
it("renders children list when note is empty", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 2");
});
it("skips archived notes in children list", async () => {
const contentEl = document.createElement("div");
const parentNote = buildNote({
title: "Parent note",
children: [
{ title: "Child note 1" },
{ title: "Child note 2", "#archived": "" },
{ title: "Child note 3" }
]
});
await renderText(parentNote, $(contentEl));
const items = contentEl.querySelectorAll("a");
expect(items.length).toBe(2);
expect(items[0].textContent).toBe("Child note 1");
expect(items[1].textContent).toBe("Child note 3");
});
});

View File

@@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
if (blob && !isHtmlEmpty(blob.content)) {
$renderedContent.append($('<div class="ck-content">').html(blob.content));
await renderIncludedNotes($renderedContent[0]);
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
if (!options.noIncludedNotes) {
await renderIncludedNotes($renderedContent[0], seenNoteIds);
} else {
$renderedContent.find("section.include-note").remove();
}
if ($renderedContent.find("span.math-tex").length > 0) {
renderMathInElement($renderedContent[0], { trust: true });
@@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote && !options.noChildrenList) {
await renderChildrenList($renderedContent, note);
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
}
}
async function renderIncludedNotes(contentEl: HTMLElement) {
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
// TODO: Consider duplicating with server's share/content_renderer.ts.
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
@@ -66,7 +73,15 @@ async function renderIncludedNotes(contentEl: HTMLElement) {
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
if (seenNoteIds.has(noteId)) {
console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`);
includeNoteEl.remove();
continue;
}
const renderedContent = (await content_renderer.getRenderedContent(note, {
seenNoteIds
})).$renderedContent;
includeNoteEl.replaceChildren(...renderedContent);
}
}
@@ -98,7 +113,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) {
}
}
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
let childNoteIds = note.getChildNoteIds();
if (!childNoteIds.length) {
@@ -108,14 +123,16 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
$renderedContent.css("padding", "10px");
$renderedContent.addClass("text-with-ellipsis");
// just load the first 10 child notes
if (childNoteIds.length > 10) {
childNoteIds = childNoteIds.slice(0, 10);
}
// just load the first 10 child notes
const childNotes = await froca.getNotes(childNoteIds);
for (const childNote of childNotes) {
if (childNote.isArchived && !includeArchivedNotes) continue;
$renderedContent.append(
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
showTooltip: false,

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from "vitest";
import { getReadableTextColor } from "./css_class_manager";
describe("getReadableTextColor", () => {
it("doesn't crash for invalid color", () => {
expect(getReadableTextColor("RandomColor")).toBe("#000");
});
it("tolerates different casing", () => {
expect(getReadableTextColor("Blue"))
.toBe(getReadableTextColor("blue"));
});
});

View File

@@ -1,21 +1,22 @@
import clsx from "clsx";
import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color";
import {readCssVar} from "../utils/css-var";
const registeredClasses = new Set<string>();
const colorsWithHue = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables
const lightThemeColorMaxLightness = readCssVar(
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
const darkThemeColorMinLightness = readCssVar(
document.documentElement,
"tree-item-dark-theme-min-color-lightness"
).asNumber(50);
document.documentElement,
"tree-item-dark-theme-min-color-lightness"
).asNumber(50);
function createClassForColor(colorString: string | null) {
if (!colorString?.trim()) return "";
@@ -27,7 +28,7 @@ function createClassForColor(colorString: string | null) {
if (!registeredClasses.has(className)) {
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
darkThemeColorMinLightness!);
darkThemeColorMinLightness!);
const hue = getHue(color);
$("head").append(`<style>
@@ -50,7 +51,7 @@ function createClassForColor(colorString: string | null) {
function parseColor(color: string) {
try {
return Color(color);
return Color(color.toLowerCase());
} catch (ex) {
console.error(ex);
}
@@ -84,8 +85,8 @@ function getHue(color: ColorInstance) {
}
export function getReadableTextColor(bgColor: string) {
const colorInstance = Color(bgColor);
return colorInstance.isLight() ? "#000" : "#fff";
const colorInstance = parseColor(bgColor);
return !colorInstance || colorInstance?.isLight() ? "#000" : "#fff";
}
export default {

View File

@@ -1,4 +1,5 @@
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
import type { AttributeType } from "../entities/fattribute.js";
import type { EntityChange } from "../server_types.js";
@@ -135,7 +136,14 @@ export default class LoadResults {
}
getBranchRows() {
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
return this.branchRows.map((row) => {
const branch = this.getEntityRow("branches", row.branchId);
if (branch) {
// Merge the componentId from the tracked row with the entity data
return { ...branch, componentId: row.componentId };
}
return null;
}).filter((branch) => !!branch) as BranchRow[];
}
addNoteReordering(parentNoteId: string, componentId: string) {
@@ -153,7 +161,14 @@ export default class LoadResults {
getAttributeRows(componentId = "none"): AttributeRow[] {
return this.attributeRows
.filter((row) => row.componentId !== componentId)
.map((row) => this.getEntityRow("attributes", row.attributeId))
.map((row) => {
const attr = this.getEntityRow("attributes", row.attributeId);
if (attr) {
// Merge the componentId from the tracked row with the entity data
return { ...attr, componentId: row.componentId };
}
return null;
})
.filter((attr) => !!attr) as AttributeRow[];
}

View File

@@ -1,6 +1,8 @@
import utils from "./utils.js";
import Component from "../components/component.js";
import FNote from "../entities/fnote.js";
import options from "./options.js";
import server from "./server.js";
import utils from "./utils.js";
type ExecFunction = (command: string, cb: (err: string, stdout: string, stderror: string) => void) => void;
@@ -36,9 +38,14 @@ function download(url: string) {
}
}
export function downloadFileNote(noteId: string) {
const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache
export function downloadFileNote(note: FNote, parentComponent: Component | null, ntxId: string | null | undefined) {
if (note.type === "file" && note.mime === "application/pdf" && parentComponent) {
// Special handling, manages its own downloading process.
parentComponent.triggerEvent("customDownload", { ntxId });
return;
}
const url = `${getFileUrl("notes", note.noteId)}?${Date.now()}`; // don't use cache
download(url);
}
@@ -97,7 +104,7 @@ async function openCustom(type: string, entityId: string, mime: string) {
// Note that the path separator must be \ instead of /
filePath = filePath.replace(/\//g, "\\");
}
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ` + filePath;
const command = `rundll32.exe shell32.dll,OpenAs_RunDLL ${filePath}`;
exec(command, (err, stdout, stderr) => {
if (err) {
console.error("Open Note custom: ", err);
@@ -131,10 +138,10 @@ export function getUrlForDownload(url: string) {
if (utils.isElectron()) {
// electron needs absolute URL, so we extract current host, port, protocol
return `${getHost()}/${url}`;
} else {
// web server can be deployed on subdomain, so we need to use a relative path
return url;
}
// web server can be deployed on subdomain, so we need to use a relative path
return url;
}
function canOpenInBrowser(mime: string) {

View File

@@ -85,13 +85,15 @@ async function remove<T>(url: string, componentId?: string) {
return await call<T>("DELETE", url, componentId);
}
async function upload(url: string, fileToUpload: File) {
async function upload(url: string, fileToUpload: File, componentId?: string) {
const formData = new FormData();
formData.append("upload", fileToUpload);
return await $.ajax({
url: window.glob.baseApiUrl + url,
headers: await getHeaders(),
headers: await getHeaders(componentId ? {
"trilium-component-id": componentId
} : undefined),
data: formData,
type: "PUT",
timeout: 60 * 60 * 1000,

View File

@@ -100,6 +100,20 @@ describe("shortcuts", () => {
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
// Option + H produces '˙'
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
// Option + S produces 'ß'
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
});
});
describe("matchesShortcut", () => {
@@ -200,6 +214,33 @@ describe("shortcuts", () => {
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
const macOSAltAEvent = createKeyboardEvent({
key: "å",
code: "KeyA",
altKey: true
});
expect(matchesShortcut(macOSAltAEvent, "alt+a")).toBe(true);
// Option/Alt + H produces '˙'
const macOSAltHEvent = createKeyboardEvent({
key: "˙",
code: "KeyH",
altKey: true
});
expect(matchesShortcut(macOSAltHEvent, "alt+h")).toBe(true);
// Combined with Ctrl: Ctrl+Alt+S where Alt produces 'ß'
const macOSCtrlAltSEvent = createKeyboardEvent({
key: "ß",
code: "KeyS",
ctrlKey: true,
altKey: true
});
expect(matchesShortcut(macOSCtrlAltSEvent, "ctrl+alt+s")).toBe(true);
});
});
describe("bindGlobalShortcut", () => {

View File

@@ -213,8 +213,11 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
}
// For letter keys, use the physical key code for consistency
// On macOS, Option/Alt key produces special characters, so we must use e.code
if (key.length === 1 && key >= 'a' && key <= 'z') {
return e.key.toLowerCase() === key.toLowerCase();
// e.code is like "KeyA", "KeyB", etc.
const expectedCode = `Key${key.toUpperCase()}`;
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
}
// For regular keys, check both key and code as fallback

View File

@@ -1,22 +1,30 @@
import type { SaveState } from "../components/note_context";
import { getErrorMessage } from "./utils";
type Callback = () => Promise<void> | void;
export type StateCallback = (state: SaveState) => void;
export default class SpacedUpdate {
private updater: Callback;
private lastUpdated: number;
private changed: boolean;
private updateInterval: number;
private changeForbidden?: boolean;
private stateCallback?: StateCallback;
constructor(updater: Callback, updateInterval = 1000) {
constructor(updater: Callback, updateInterval = 1000, stateCallback?: StateCallback) {
this.updater = updater;
this.lastUpdated = Date.now();
this.changed = false;
this.updateInterval = updateInterval;
this.stateCallback = stateCallback;
}
scheduleUpdate() {
if (!this.changeForbidden) {
this.changed = true;
this.stateCallback?.("unsaved");
setTimeout(() => this.triggerUpdate());
}
}
@@ -26,10 +34,13 @@ export default class SpacedUpdate {
this.changed = false; // optimistic...
try {
this.stateCallback?.("saving");
await this.updater();
this.stateCallback?.("saved");
} catch (e) {
this.changed = true;
this.stateCallback?.("error");
logError(getErrorMessage(e));
throw e;
}
}
@@ -59,15 +70,22 @@ export default class SpacedUpdate {
this.updateInterval = interval;
}
triggerUpdate() {
async triggerUpdate() {
if (!this.changed) {
return;
}
if (Date.now() - this.lastUpdated > this.updateInterval) {
this.updater();
this.stateCallback?.("saving");
try {
await this.updater();
this.stateCallback?.("saved");
this.changed = false;
} catch (e) {
this.stateCallback?.("error");
logError(getErrorMessage(e));
}
this.lastUpdated = Date.now();
this.changed = false;
} else {
// update isn't triggered but changes are still pending, so we need to schedule another check
this.scheduleUpdate();

View File

@@ -187,13 +187,15 @@ export function formatSize(size: number | null | undefined) {
return "";
}
size = Math.max(Math.round(size / 1024), 1);
if (size < 1024) {
return `${size} KiB`;
if (size === 0) {
return "0 B";
}
return `${Math.round(size / 102.4) / 10} MiB`;
const k = 1024;
const sizes = ["B", "KiB", "MiB", "GiB"];
const i = Math.floor(Math.log(size) / Math.log(k));
return `${Math.round((size / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`;
}
function toObject<T, R>(array: T[], fn: (arg0: T) => [key: string, value: R]) {

View File

@@ -17,6 +17,8 @@
*/
:root {
color-scheme: var(--theme-style);
--main-font-family: "Inter", sans-serif;
--main-font-size: normal;

View File

@@ -767,7 +767,7 @@ body.mobile .fancytree-node > span {
background: var(--left-pane-item-hover-background);
}
#left-pane span.fancytree-node.shared .fancytree-title::after {
#left-pane .note-indicator-icon.shared-indicator {
opacity: 0.5;
}
@@ -1259,8 +1259,16 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 250ms ease-in;
border: 2px solid transparent;
}
/* The active split in a multi-split view */
#center-pane > .split-note-container-widget:has(> .note-split.visible ~ .note-split.visible) > .note-split.active {
border-color: var(--link-selection-outline-color);
}
body:not(.background-effects) #center-pane .note-split {
animation: note-entrance 100ms linear;
}

View File

@@ -148,29 +148,28 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
/* Note indicator icons (clone, shared) - real DOM elements for tooltip support */
.note-indicator-icon {
font-family: "boxicons" !important;
font-size: smaller;
content: " \eb3d\ec03";
margin-inline-start: 4px;
opacity: 0.8;
cursor: help;
}
span.fancytree-node.multiple-parents .fancytree-title::after {
font-family: "boxicons" !important;
font-size: smaller;
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
.note-indicator-icon.clone-indicator::before {
content: "\eb3d"; /* bx-link-alt */
}
body.experimental-feature-new-layout span.fancytree-node.multiple-parents .fancytree-title::after {
content: " \ed82";
.note-indicator-icon.shared-indicator::before {
content: "\ec03"; /* bx-share-alt */
}
body.experimental-feature-new-layout .note-indicator-icon.clone-indicator::before {
content: "\ed82";
opacity: 0.5;
}
span.fancytree-node.shared .fancytree-title::after {
font-family: "boxicons" !important;
font-size: smaller;
content: " \ec03"; /* lookup code for "share-alt" in boxicons.css */
}
span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-title {
font-weight: bold;
}

View File

@@ -69,24 +69,6 @@ export function buildNote(noteDef: NoteDefinition) {
});
note.getBlob = async () => blob;
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNotePosition += 10;
}
}
let position = 0;
for (const [ key, value ] of Object.entries(noteDef)) {
const attributeId = utils.randomString(12);
@@ -136,5 +118,25 @@ export function buildNote(noteDef: NoteDefinition) {
}
noteAttributeCache.attributes[note.noteId].push(attribute);
}
// Manage children.
if (noteDef.children) {
for (const childDef of noteDef.children) {
const childNote = buildNote(childDef);
const branchId = `${note.noteId}_${childNote.noteId}`;
const branch = new FBranch(froca, {
branchId,
noteId: childNote.noteId,
parentNoteId: note.noteId,
notePosition: childNotePosition,
fromSearchNote: false
});
froca.branches[branchId] = branch;
note.addChild(childNote.noteId, branchId, false);
childNote.addParent(note.noteId, branchId, false);
childNotePosition += 10;
}
}
return note;
}

View File

@@ -484,7 +484,6 @@
"delete_button": "حذف",
"download_button": "تنزيل",
"restore_button": "أستعادة",
"preview": "معاينة:",
"note_revisions": "مراجعات الملاحظة",
"diff_on": "عرض الفروقات",
"diff_off": "عرض المحتوى",

View File

@@ -64,8 +64,7 @@
"restore_button": "Restaura",
"delete_button": "Suprimeix",
"download_button": "Descarrega",
"mime": "MIME: ",
"preview": "Vista prèvia:"
"mime": "MIME: "
},
"sort_child_notes": {
"title": "títol",

View File

@@ -290,7 +290,6 @@
"download_button": "下载",
"mime": "MIME 类型: ",
"file_size": "文件大小:",
"preview": "预览:",
"preview_not_available": "无法预览此类型的笔记。",
"diff_on": "显示差异",
"diff_off": "显示内容",
@@ -765,7 +764,14 @@
"note_icon": {
"change_note_icon": "更改笔记图标",
"search": "搜索:",
"reset-default": "重置为默认图标"
"reset-default": "重置为默认图标",
"search_placeholder_other": "在 {{count}} 个图标包中搜索 {{number}} 个图标",
"search_placeholder_filtered": "在 {{name}} 中搜索 {{number}} 个图标",
"filter": "筛选",
"filter-none": "所有图标",
"filter-default": "默认图标",
"icon_tooltip": "{{name}}\n图标包{{iconPack}}",
"no_results": "没有找到图标。"
},
"basic_properties": {
"note_type": "笔记类型",
@@ -792,7 +798,8 @@
"expand_tooltip": "展开此集合的直接子代(单层深度)。点击右方箭头以查看更多选项。",
"expand_first_level": "展开直接子代",
"expand_nth_level": "展开 {{depth}} 层",
"expand_all_levels": "展开所有层级"
"expand_all_levels": "展开所有层级",
"hide_child_notes": "隐藏树中的子笔记"
},
"edited_notes": {
"no_edited_notes_found": "今天还没有编辑过的笔记...",
@@ -1445,7 +1452,7 @@
"will_be_deleted_in": "此附件将在 {{time}} 后自动删除",
"will_be_deleted_soon": "该附件在不久后将被自动删除",
"deletion_reason": ",因为该附件未链接在笔记的内容中。为防止被删除,请将附件链接重新添加到内容中或将附件转换为笔记。",
"role_and_size": "角色:{{role}},大小:{{size}}",
"role_and_size": "角色:{{role}},大小:{{size}},文件类型:{{- mimeType}}",
"link_copied": "附件链接已复制到剪贴板。",
"unrecognized_role": "无法识别的附件角色 '{{role}}'。"
},
@@ -1499,7 +1506,10 @@
"duplicate": "复制",
"open-in-popup": "快速编辑",
"archive": "归档",
"unarchive": "解压"
"unarchive": "解压",
"open-in-a-new-window": "在新窗口中打开",
"hide-subtree": "隐藏子树",
"show-subtree": "显示子树"
},
"shared_info": {
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。",
@@ -1588,7 +1598,15 @@
"create-child-note": "创建子笔记",
"unhoist": "取消聚焦",
"toggle-sidebar": "切换侧边栏",
"dropping-not-allowed": "不允许移动笔记到此处。"
"dropping-not-allowed": "不允许移动笔记到此处。",
"shared-indicator-tooltip": "此笔记已公开分享",
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
"clone-indicator-tooltip-single": "此笔记已克隆1 个额外的父级:{{- parent}}",
"subtree-hidden-tooltip_other": "从树中隐藏的 {{count}} 篇子笔记",
"subtree-hidden-moved-title": "已添加到 {{title}}",
"subtree-hidden-moved-description-collection": "此集合隐藏其树中的子笔记。",
"subtree-hidden-moved-description-other": "子笔记隐藏于此笔记的树中。"
},
"title_bar_buttons": {
"window-on-top": "保持此窗口置顶"
@@ -2186,7 +2204,14 @@
"execute_sql_description": "这是一篇 SQL 笔记。点击即可执行 SQL 查询。",
"shared_copy_to_clipboard": "复制链接到剪贴板",
"shared_open_in_browser": "在浏览器中打开链接",
"shared_unshare": "取消共享"
"shared_unshare": "取消共享",
"save_status_saved": "已保存",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存失败",
"save_status_unsaved_tooltip": "还有一些更改尚未保存。它们将稍后自动保存。",
"save_status_error_tooltip": "保存笔记时出错。如果可以,请尝试将笔记内容复制到其他位置并重新加载应用程序。",
"save_status_saving_tooltip": "更改正在保存。"
},
"status_bar": {
"language_title": "更改内容语言",
@@ -2217,5 +2242,12 @@
},
"attributes_panel": {
"title": "笔记属性"
},
"pdf": {
"attachments_other": "{{count}} 个附件",
"pages_other": "共{{count}}页",
"pages_alt": "第{{pageNumber}}页",
"pages_loading": "加载中...",
"layers_other": "{{count}} 层"
}
}

View File

@@ -1,6 +1,6 @@
{
"about": {
"title": "Über Trilium Notes",
"title": "Über Trilium Notizen",
"homepage": "Startseite:",
"app_version": "App-Version:",
"db_version": "DB-Version:",
@@ -25,7 +25,13 @@
},
"widget-list-error": {
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
}
},
"open-script-note": "Script-Notiz öffnen",
"widget-render-error": {
"title": "Eine externe React Integration konnte nicht dargestellt werden"
},
"widget-missing-parent": "Der externen Integration fehlt die erforderliche Eigenschaft '{{property}}'\n\nFalls dieses Skript ohne UI-Element ausgeführt werden soll, benutze stattdessen '#run=frontendStartup'.",
"scripting-error": "Benutzerdefinierter Skriptfehler: {{title}}"
},
"add_link": {
"add_link": "Link hinzufügen",
@@ -208,7 +214,8 @@
"info": {
"modalTitle": "Infonachricht",
"closeButton": "Schließen",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "In Zwischenablage kopieren"
},
"jump_to_note": {
"search_button": "Suche im Volltext",
@@ -281,7 +288,6 @@
"download_button": "Herunterladen",
"mime": "MIME: ",
"file_size": "Dateigröße:",
"preview": "Vorschau:",
"preview_not_available": "Für diesen Notiztyp ist keine Vorschau verfügbar.",
"restore_button": "Wiederherstellen",
"delete_button": "Löschen",
@@ -692,7 +698,13 @@
"convert_into_attachment_successful": "Notiz '{{title}}' wurde als Anhang konvertiert.",
"convert_into_attachment_prompt": "Bist du dir sicher, dass du die Notiz '{{title}}' in ein Anhang der übergeordneten Notiz konvertieren möchtest?",
"print_pdf": "Export als PDF...",
"open_note_on_server": "Öffne Notiz auf dem Server"
"open_note_on_server": "Öffne Notiz auf dem Server",
"export_as_image": "Als Bild exportieren",
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte",
"view_revisions": "Änderungshistorie...",
"advanced": "Fortgeschritten"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
@@ -750,7 +762,15 @@
"note_icon": {
"change_note_icon": "Notiz-Icon ändern",
"search": "Suche:",
"reset-default": "Standard wiederherstellen"
"reset-default": "Standard wiederherstellen",
"search_placeholder_one": "Suche {{number}} Icons über {{count}} Pakete",
"search_placeholder_other": "Suche {{number}} Icons über {{count}} Pakete",
"search_placeholder_filtered": "Suche {{number}} Icons in {{name}}",
"filter": "Filter",
"filter-none": "Alle Icons",
"filter-default": "Standard Icons",
"icon_tooltip": "{{name}}\nIcon Paket: {{iconPack}}",
"no_results": "Keine Icons gefunden."
},
"basic_properties": {
"note_type": "Notiztyp",
@@ -777,7 +797,8 @@
"expand_all_levels": "Alle Ebenen erweitern",
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
"expand_first_level": "Direkte Unterelemente erweitern",
"expand_nth_level": "{{depth}} Ebenen erweitern"
"expand_nth_level": "{{depth}} Ebenen erweitern",
"hide_child_notes": "Unterknoten im Baum ausblenden"
},
"edited_notes": {
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
@@ -790,7 +811,7 @@
"file_type": "Dateityp",
"file_size": "Dateigröße",
"download": "Herunterladen",
"open": "Offen",
"open": "Extern öffnen",
"upload_new_revision": "Neue Revision hochladen",
"upload_success": "Neue Dateirevision wurde hochgeladen.",
"upload_failed": "Das Hochladen einer neuen Dateirevision ist fehlgeschlagen.",
@@ -810,7 +831,8 @@
},
"inherited_attribute_list": {
"title": "Geerbte Attribute",
"no_inherited_attributes": "Keine geerbten Attribute."
"no_inherited_attributes": "Keine geerbten Attribute.",
"none": "Keine"
},
"note_info_widget": {
"note_id": "Notiz-ID",
@@ -821,7 +843,9 @@
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
"calculate": "berechnen",
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
"title": "Notizinfo"
"title": "Notizinfo",
"mime": "MIME Typ",
"show_similar_notes": "Zeige ähnliche Notizen"
},
"note_map": {
"open_full": "Vollständig erweitern",
@@ -884,7 +908,8 @@
"search_parameters": "Suchparameter",
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
"actions_executed": "Aktionen wurden ausgeführt."
"actions_executed": "Aktionen wurden ausgeführt.",
"view_options": "Optionen anzeigen:"
},
"similar_notes": {
"title": "Ähnliche Notizen",
@@ -988,7 +1013,12 @@
"editable_text": {
"placeholder": "Gebe hier den Inhalt deiner Notiz ein...",
"auto-detect-language": "Automatisch erkannt",
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht."
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
"editor_crashed_title": "Der Text Editor ist abgestürzt",
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber kürzlich gemachte Änderungen wurden unter Umständen nicht gespeichert.",
"editor_crashed_details_button": "Mehr Details anzeigen...",
"editor_crashed_details_intro": "Falls dieser Fehler häufiger auftritt, ziehen Sie in Betracht uns diesen über GitHub zu melden, indem Sie die folgenden Informationen bereitstellen.",
"editor_crashed_details_title": "Technische Informationen"
},
"empty": {
"open_note_instruction": "Öffne eine Notiz, indem du den Titel der Notiz in die Eingabe unten eingibst oder eine Notiz in der Baumstruktur auswählst.",
@@ -1388,7 +1418,7 @@
"will_be_deleted_in": "Dieser Anhang wird in {{time}} automatisch gelöscht",
"will_be_deleted_soon": "Dieser Anhang wird bald automatisch gelöscht",
"deletion_reason": ", da der Anhang nicht im Inhalt der Notiz verlinkt ist. Um das Löschen zu verhindern, füge den Anhangslink wieder in den Inhalt ein oder wandel den Anhang in eine Notiz um.",
"role_and_size": "Rolle: {{role}}, Größe: {{size}}",
"role_and_size": "Rolle: {{role}}, Größe: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Anhangslink in die Zwischenablage kopiert.",
"unrecognized_role": "Unbekannte Anhangsrolle „{{role}}“."
},
@@ -1439,10 +1469,13 @@
"import-into-note": "In Notiz importieren",
"apply-bulk-actions": "Massenaktionen anwenden",
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?",
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest? Diese Operation wird nur auf Bildnotizes angewandt. Andere Notizen werden übersprungen.",
"open-in-popup": "Schnellbearbeitung",
"archive": "Archiviere",
"unarchive": "Entarchivieren"
"unarchive": "Entarchivieren",
"open-in-a-new-window": "In neuem Fenster öffnen",
"hide-subtree": "Teilbaum ausblenden",
"show-subtree": "Teilbaum anzeigen"
},
"shared_info": {
"shared_publicly": "Diese Notiz ist öffentlich geteilt auf {{- link}}.",
@@ -1503,7 +1536,12 @@
},
"highlights_list_2": {
"title": "Hervorhebungs-Liste",
"options": "Optionen"
"options": "Optionen",
"title_with_count_one": "{{count}} Highlight",
"title_with_count_other": "{{count}} Highlights",
"modal_title": "Highlight Liste konfigurieren",
"menu_configure": "Highlight Liste konfigurieren…",
"no_highlights": "Keine Highlights gefunden."
},
"quick-search": {
"placeholder": "Schnellsuche",
@@ -1527,7 +1565,16 @@
"create-child-note": "Unternotiz anlegen",
"unhoist": "Fokus verlassen",
"toggle-sidebar": "Seitenleiste ein-/ausblenden",
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig."
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig.",
"clone-indicator-tooltip": "Diese Notiz hat {{- count}} Elterknoten: {{- parents}}",
"clone-indicator-tooltip-single": "Diese Notiz ist geklont (1 weiterer Elternknoten: {{- parent}})",
"shared-indicator-tooltip": "Diese Notiz ist öffentlich einsehbar",
"shared-indicator-tooltip-with-url": "Diese Notiz ist unter {{- url}} öffentlich einsehbar",
"subtree-hidden-tooltip_one": "{{count}} Unterknoten, der im Baum ausgeblendet ist",
"subtree-hidden-tooltip_other": "{{count}} Unterknoten, die im Baum ausgeblendet sind",
"subtree-hidden-moved-title": "Zu {{title}} hinzugefügt",
"subtree-hidden-moved-description-collection": "Diese Sammlung blendet ihre Unternotizem im Baum aus.",
"subtree-hidden-moved-description-other": "Diese Sammlung blendet ihre Unterknoten im Baum aus."
},
"title_bar_buttons": {
"window-on-top": "Dieses Fenster immer oben halten"
@@ -1535,10 +1582,23 @@
"note_detail": {
"could_not_find_typewidget": "Konnte typeWidget für Typ {{type}} nicht finden",
"printing": "Druckvorgang läuft…",
"printing_pdf": "PDF-Export läuft…"
"printing_pdf": "PDF-Export läuft…",
"print_report_title": "Druckreport",
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt ist oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt sind oder geschützt sind."
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…"
"placeholder": "Titel der Notiz hier eingeben…",
"created_on": "Erstellt am <Value />",
"last_modified": "Bearbeitet am <Value />",
"note_type_switcher_label": "Ändere von {{type}} zu:",
"note_type_switcher_others": "Andere Notizart",
"note_type_switcher_templates": "Template",
"note_type_switcher_collection": "Sammlung",
"edited_notes": "Notizen, bearbeitet an diesem Tag",
"promoted_attributes": "Hervorgehobene Attribute"
},
"search_result": {
"no_notes_found": "Es wurden keine Notizen mit den angegebenen Suchparametern gefunden.",
@@ -1567,7 +1627,8 @@
},
"toc": {
"table_of_contents": "Inhaltsverzeichnis",
"options": "Optionen"
"options": "Optionen",
"no_headings": "Keine Überschriften."
},
"watched_file_update_status": {
"file_last_modified": "Datei <code class=\"file-path\"></code> wurde zuletzt geändert am <span class=\"file-last-modified\"></span>.",
@@ -1679,7 +1740,8 @@
"open_note_in_new_tab": "Notiz in neuen Tab öffnen",
"open_note_in_new_split": "Notiz in neuen geteilten Tab öffnen",
"open_note_in_new_window": "Notiz in neuen Fenster öffnen",
"open_note_in_popup": "Schnellbearbeitung"
"open_note_in_popup": "Schnellbearbeitung",
"open_note_in_other_split": "Notiz in neuer Spalte öffnen"
},
"electron_integration": {
"desktop-application": "Desktop Anwendung",
@@ -1947,8 +2009,9 @@
"unknown_widget": "Unbekanntes Widget für '{{id}}'."
},
"note_language": {
"not_set": "Nicht gesetzt",
"configure-languages": "Konfiguriere Sprachen..."
"not_set": "Keine Sprache ausgewählt",
"configure-languages": "Konfiguriere Sprachen...",
"help-on-languages": "Zu Übersetzungen beitragen..."
},
"content_language": {
"title": "Inhaltssprachen",
@@ -1966,7 +2029,8 @@
"button_title": "Exportiere Diagramm als PNG"
},
"svg": {
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden."
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden.",
"export_to_svg": "Das Diagramm konnte nicht als SVG exportiert werden."
},
"code_theme": {
"title": "Aussehen",
@@ -2014,7 +2078,7 @@
"book_properties_config": {
"hide-weekends": "Wochenenden ausblenden",
"display-week-numbers": "Zeige Kalenderwoche",
"map-style": "Kartenstil:",
"map-style": "Kartenstil",
"max-nesting-depth": "Maximale Verschachtelungstiefe:",
"raster": "Raster",
"vector_light": "Vektor (Hell)",
@@ -2067,14 +2131,20 @@
"background_effects_title": "Hintergrundeffekte sind jetzt zuverlässig nutzbar",
"background_effects_message": "Auf Windows-Geräten sind die Hintergrundeffekte nun vollständig stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird. Diese Technik wird auch in anderen Anwendungen wie dem Windows-Explorer eingesetzt.",
"background_effects_button": "Aktiviere Hintergrundeffekte",
"dismiss": "Ablehnen"
"dismiss": "Ablehnen",
"new_layout_title": "Neues Layout",
"new_layout_message": "Wir haben ein modernisiertes Layout für Trilium eingeführt. Die Multifunktionsleiste wurde entfernt und als neue Statusanzeige und ausklappbaren Sektionen (wie hervorgehobenen Attributen), welche Schlüsselfunktionen übernehmen, nahtlos in das Hauptinterface integriert.\n\nDas neue Layout ist standardmäßig aktiviert und kann temporär in Optionen → Anzeige deaktiviert werden.",
"new_layout_button": "Mehr Informationen"
},
"settings": {
"related_settings": "Ähnliche Einstellungen"
},
"settings_appearance": {
"related_code_blocks": "Farbschema für Code-Blöcke in Textnotizen",
"related_code_notes": "Farbschema für Code-Notizen"
"related_code_notes": "Farbschema für Code-Notizen",
"ui": "Benutzeroberfläche",
"ui_old_layout": "Altes Layout",
"ui_new_layout": "Neues Layout"
},
"units": {
"percentage": "%"
@@ -2106,5 +2176,92 @@
},
"popup-editor": {
"maximize": "Wechsele zum vollständigen Editor"
},
"experimental_features": {
"title": "Experimentelle Optionen",
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
"new_layout_name": "Neues Layout",
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern."
},
"server": {
"unknown_http_error_title": "Bei der Kommunikation mit dem Server ist ein Fehler aufgetreten",
"unknown_http_error_content": "Statuscode: {{statusCode}}\nURL: {{method}} {{url}}\nNachricht: {{message}}",
"traefik_blocks_requests": "Der Traefik Reverse-Proxy hat ein fatales Update bekommen, welche die Kommunikation mit dem Server stört."
},
"tab_history_navigation_buttons": {
"go-back": "Zur vorherigen Notiz zurück kehren",
"go-forward": "Zur nächsten Notiz"
},
"breadcrumb": {
"hoisted_badge": "Gehoben",
"hoisted_badge_title": "Abgesenkt",
"workspace_badge": "Arbeitsfläche",
"scroll_to_top_title": "Zum Anfang der Notiz springen",
"create_new_note": "Neue Unternotiz erstellen",
"empty_hide_archived_notes": "Archivierte Notizen ausblenden"
},
"breadcrumb_badges": {
"read_only_explicit": "Nicht Änderbar",
"read_only_explicit_description": "Diese Notiz wurde händisch als nicht änderbar markiert.\nKlicke hier um sie temporär zu bearbeiten.",
"read_only_auto": "Automatisch nicht änderbar",
"read_only_auto_description": "Diese Notiz wurde automatisch aus Leistungsgründen als nicht änderbar markiert. Dieses automatische Limit kann in den Einstellungen angepasst werden.\n\nKlicke hier, um sie temporär zu bearbeiten.",
"read_only_temporarily_disabled": "Temporär bearbeitbar",
"read_only_temporarily_disabled_description": "Diese Notiz ist aktuell bearbeitbar, ist aber normalerweise nicht änderbar. Sobald du zu einer anderen Notiz navigierst, kehrt diese Notiz in ihren Normalzustand zurück.\n\nKlicke hier, um die Notiz wieder nicht änderbar zu machen.",
"shared_publicly": "Öffentlich geteilt",
"shared_locally": "Lokal geteilt",
"shared_copy_to_clipboard": "Link in die Zwischenablage kopieren",
"shared_open_in_browser": "Link öffnen",
"shared_unshare": "Teilen aufheben",
"clipped_note": "Internetschnellverweis",
"clipped_note_description": "Diese Notiz wurde von {{url}} übernommen.\n\nKlicke hier, um zum Ursprung zu gehen.",
"execute_script": "Skript ausführen",
"execute_script_description": "Diese Notiz ist eine Skriptnotiz. Klicke hier, um das Skript auszuführen.",
"execute_sql": "SQL ausführen",
"execute_sql_description": "Diese Notiz ist eine SQL-Notiz. Klicke hier, um die SQL-Abfrage auszuführen.",
"save_status_saved": "Gespeichert",
"save_status_saving": "Speichern...",
"save_status_unsaved": "Nicht gespeichert",
"save_status_error": "Speichern fehlgeschlagen",
"save_status_saving_tooltip": "Änderungen werden gespeichert.",
"save_status_unsaved_tooltip": "Es gibt ungespeicherte Änderungen, welche gleich automatisch gespeichert werden.",
"save_status_error_tooltip": "Beim speichern der Notiz ist ein Fehler aufgetreten. Wenn möglich, versuche die Notiz woandershin zu kopieren und die Applikation neu zu laden."
},
"status_bar": {
"language_title": "Inhaltssprache ändern",
"note_info_title": "Notizinfo anzeigen (z.B.: Datum, Notizgröße)",
"backlinks_one": "{{count}} Rücklink",
"backlinks_other": "{{count}} Rücklinks",
"backlinks_title_one": "Rücklink anzeigen",
"backlinks_title_other": "Rücklinks anzeigen",
"attachments_one": "{{count}} Anhang",
"attachments_other": "{{count}} Anhänge",
"attachments_title_one": "Anhang in einem neuen Tab öffnen",
"attachments_title_other": "Anhänge in einem neuen Tab öffnen",
"attributes_one": "{{count}} Eigenschaft",
"attributes_other": "{{count}} Eigenschaften",
"attributes_title": "Eigene und gererbte Eigenschaften",
"note_paths_one": "{{count}} Pfad",
"note_paths_other": "{{count}} Pfade",
"note_paths_title": "Notizpfade",
"code_note_switcher": "Sprachmodus ändern"
},
"attributes_panel": {
"title": "Notizeigenschaften"
},
"right_pane": {
"empty_message": "Für diese Notiz gibt es nichts anzuzeigen",
"empty_button": "Anzeige ausblenden",
"toggle": "Rechte Anzeige umschalten",
"custom_widget_go_to_source": "Zum Ursprungscode"
},
"pdf": {
"attachments_one": "{{count}} Anhang",
"attachments_other": "{{count}} Anhänge",
"layers_one": "{{count}} Ebene",
"layers_other": "{{count}} Ebenen",
"pages_one": "{{count}} Seite",
"pages_other": "{{count}} Seiten",
"pages_alt": "Seite {{pageNumber}}",
"pages_loading": "Laden..."
}
}

View File

@@ -69,5 +69,8 @@
"clear-color": "Clear note colour",
"set-color": "Set note colour",
"set-custom-color": "Set custom note colour"
},
"about": {
"title": "About Trilium Notes"
}
}

View File

@@ -295,7 +295,6 @@
"download_button": "Download",
"mime": "MIME: ",
"file_size": "File size:",
"preview": "Preview:",
"preview_not_available": "Preview isn't available for this note type."
},
"sort_child_notes": {
@@ -801,7 +800,8 @@
"geo-map": "Geo Map",
"board": "Board",
"presentation": "Presentation",
"include_archived_notes": "Show archived notes"
"include_archived_notes": "Show archived notes",
"hide_child_notes": "Hide child notes in tree"
},
"edited_notes": {
"no_edited_notes_found": "No edited notes on this day yet...",
@@ -1644,6 +1644,7 @@
"tree-context-menu": {
"open-in-a-new-tab": "Open in a new tab",
"open-in-a-new-split": "Open in a new split",
"open-in-a-new-window": "Open in a new window",
"insert-note-after": "Insert note after",
"insert-child-note": "Insert child note",
"archive": "Archive",
@@ -1656,6 +1657,8 @@
"advanced": "Advanced",
"expand-subtree": "Expand subtree",
"collapse-subtree": "Collapse subtree",
"hide-subtree": "Hide subtree",
"show-subtree": "Show subtree",
"sort-by": "Sort by...",
"recent-changes-in-subtree": "Recent changes in subtree",
"convert-to-attachment": "Convert to attachment",
@@ -1769,7 +1772,16 @@
"create-child-note": "Create child note",
"unhoist": "Unhoist",
"toggle-sidebar": "Toggle sidebar",
"dropping-not-allowed": "Dropping notes into this location is not allowed."
"dropping-not-allowed": "Dropping notes into this location is not allowed.",
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
"shared-indicator-tooltip": "This note is shared publicly",
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} child note that is hidden from the tree",
"subtree-hidden-tooltip_other": "{{count}} child notes that are hidden from the tree",
"subtree-hidden-moved-title": "Added to {{title}}",
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
},
"title_bar_buttons": {
"window-on-top": "Keep Window on Top"
@@ -2205,7 +2217,14 @@
"execute_script": "Run script",
"execute_script_description": "This note is a script note. Click to execute the script.",
"execute_sql": "Run SQL",
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query."
"execute_sql_description": "This note is a SQL note. Click to execute the SQL query.",
"save_status_saved": "Saved",
"save_status_saving": "Saving...",
"save_status_unsaved": "Unsaved",
"save_status_error": "Save failed",
"save_status_saving_tooltip": "Changes are being saved.",
"save_status_unsaved_tooltip": "There are unsaved changes. They will be saved automatically in a moment.",
"save_status_error_tooltip": "An error occurred while saving the note. If possible, try copying the note content elsewhere and reloading the application."
},
"status_bar": {
"language_title": "Change content language",
@@ -2234,5 +2253,15 @@
"empty_button": "Hide the panel",
"toggle": "Toggle right panel",
"custom_widget_go_to_source": "Go to source code"
},
"pdf": {
"attachments_one": "{{count}} attachment",
"attachments_other": "{{count}} attachments",
"layers_one": "{{count}} layer",
"layers_other": "{{count}} layers",
"pages_one": "{{count}} page",
"pages_other": "{{count}} pages",
"pages_alt": "Page {{pageNumber}}",
"pages_loading": "Loading..."
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Hubo un fallo al cargar un script personalizado",
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
"message": "El script no pudo ser ejecutado debido a:\n\n{{message}}"
},
"widget-list-error": {
"title": "Hubo un fallo al obtener la lista de widgets del servidor"
},
"widget-render-error": {
"title": "Hubo un fallo al renderizar un widget personalizado de React"
}
},
"add_link": {
@@ -162,7 +168,8 @@
"other": "Otro",
"quickSearch": "centrarse en la entrada de búsqueda rápida",
"inPageSearch": "búsqueda en la página",
"title": "Hoja de ayuda"
"title": "Hoja de ayuda",
"editShortcuts": "Editar atajos de teclado"
},
"import": {
"importIntoNote": "Importar a nota",
@@ -279,7 +286,6 @@
"download_button": "Descargar",
"mime": "MIME: ",
"file_size": "Tamaño del archivo:",
"preview": "Vista previa:",
"preview_not_available": "La vista previa no está disponible para este tipo de notas.",
"diff_off": "Mostrar contenido",
"diff_on": "Mostrar diferencia",
@@ -691,7 +697,7 @@
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
"print_pdf": "Exportar como PDF...",
"open_note_on_server": "Abrir nota en el servidor"
"open_note_on_server": "Abrir nota en servidor"
},
"onclick_button": {
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
@@ -737,7 +743,7 @@
"zpetne_odkazy": {
"relation": "relación",
"backlink_one": "{{count}} Vínculo de retroceso",
"backlink_many": "",
"backlink_many": "{{count}} Vínculos de retroceso",
"backlink_other": "{{count}} vínculos de retroceso"
},
"mobile_detail_menu": {
@@ -750,7 +756,10 @@
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
"search": "Búsqueda:",
"reset-default": "Restablecer a icono por defecto"
"reset-default": "Restablecer a icono por defecto",
"search_placeholder_one": "Buscar {{number}} icono a través de {{count}} paquetes",
"search_placeholder_many": "Buscar {{number}} iconos a través de {{count}} paquetes",
"search_placeholder_other": "Buscar {{number}} iconos a través de {{count}} paquetes"
},
"basic_properties": {
"note_type": "Tipo de nota",
@@ -790,7 +799,7 @@
"file_type": "Tipo de archivo",
"file_size": "Tamaño del archivo",
"download": "Descargar",
"open": "Abrir",
"open": "Abrir externamente",
"upload_new_revision": "Subir nueva revisión",
"upload_success": "Se ha subido una nueva revisión de archivo.",
"upload_failed": "Error al cargar una nueva revisión de archivo.",
@@ -1303,11 +1312,11 @@
"code_mime_types": {
"title": "Tipos MIME disponibles en el menú desplegable",
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
"tooltip_code_block_syntax": "Bloques de código en notas de texto",
"tooltip_code_note_syntax": "Notas de código"
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
"tooltip_code_note_syntax": "Notas de Código"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Atajos de teclas de Vim",
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
"enable_vim_keybindings": "Habilitar los atajos de teclas de Vim en la notas de código (no es modo ex)"
},
"wrap_lines": {
@@ -1572,7 +1581,7 @@
"will_be_deleted_in": "Este archivo adjunto se eliminará automáticamente en {{time}}",
"will_be_deleted_soon": "Este archivo adjunto se eliminará automáticamente pronto",
"deletion_reason": ", porque el archivo adjunto no está vinculado en el contenido de la nota. Para evitar la eliminación, vuelva a agregar el enlace del archivo adjunto al contenido o convierta el archivo adjunto en una nota.",
"role_and_size": "Rol: {{role}}, Tamaño: {{size}}",
"role_and_size": "Rol: {{role}}, tamaño: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Enlace del archivo adjunto copiado al portapapeles.",
"unrecognized_role": "Rol de archivo adjunto no reconocido '{{role}}'."
},
@@ -1623,7 +1632,7 @@
"import-into-note": "Importar a nota",
"apply-bulk-actions": "Aplicar acciones en lote",
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?",
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
"open-in-popup": "Edición rápida",
"archive": "Archivar",
"unarchive": "Desarchivar"
@@ -1718,7 +1727,10 @@
"note_detail": {
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
"printing": "Impresión en curso...",
"printing_pdf": "Exportando a PDF en curso.."
"printing_pdf": "Exportando a PDF en curso..",
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
},
"note_title": {
"placeholder": "escriba el título de la nota aquí..."
@@ -1930,7 +1942,7 @@
"unknown_widget": "Widget desconocido para \"{{id}}\"."
},
"note_language": {
"not_set": "No establecido",
"not_set": "Idioma no establecido",
"configure-languages": "Configurar idiomas..."
},
"content_language": {
@@ -1969,7 +1981,7 @@
"hide-weekends": "Ocultar fines de semana",
"show-scale": "Mostrar escala",
"display-week-numbers": "Mostrar números de semana",
"map-style": "Estilo de mapa:",
"map-style": "Estilo de mapa",
"max-nesting-depth": "Máxima profundidad de anidamiento:",
"vector_light": "Vector (claro)",
"vector_dark": "Vector (oscuro)",
@@ -2098,5 +2110,36 @@
"clear-color": "Borrar color de nota",
"set-color": "Asignar color de nota",
"set-custom-color": "Asignar color de nota personalizado"
},
"status_bar": {
"backlinks_one": "{{count}} vínculo de retroceso",
"backlinks_many": "{{count}} vínculos de retroceso",
"backlinks_other": "{{count}} vínculos de retroceso",
"backlinks_title_one": "Ver vínculo de retroceso",
"backlinks_title_many": "Ver vínculos de retroceso",
"backlinks_title_other": "Ver vínculos de retroceso",
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"attachments_title_one": "Ver adjunto en una nueva pestaña",
"attachments_title_many": "Ver adjuntos en una nueva pestaña",
"attachments_title_other": "Ver adjuntos en una nueva pestaña",
"attributes_one": "{{count}} atributo",
"attributes_many": "{{count}} atributos",
"attributes_other": "{{count}} atributos",
"note_paths_one": "{{count}} ruta",
"note_paths_many": "{{count}} rutas",
"note_paths_other": "{{count}} rutas"
},
"pdf": {
"attachments_one": "{{count}} adjunto",
"attachments_many": "{{count}} adjuntos",
"attachments_other": "{{count}} adjuntos",
"layers_one": "{{count}} capa",
"layers_many": "{{count}} capas",
"layers_other": "{{count}} capas",
"pages_one": "{{count}} página",
"pages_many": "{{count}} páginas",
"pages_other": "{{count}} páginas"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Echec du chargement d'un script personnalisé",
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
},
"widget-list-error": {
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
},
"widget-render-error": {
"title": "Rendu impossible d'un widget React custom"
}
},
"add_link": {
@@ -279,7 +285,6 @@
"download_button": "Télécharger",
"mime": "MIME : ",
"file_size": "Taille du fichier :",
"preview": "Aperçu :",
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
"restore_button": "Restaurer",
"delete_button": "Supprimer",
@@ -757,7 +762,11 @@
"note_icon": {
"change_note_icon": "Changer l'icône de note",
"search": "Recherche :",
"reset-default": "Réinitialiser l'icône par défaut"
"reset-default": "Réinitialiser l'icône par défaut",
"filter": "Filtre",
"filter-none": "Toutes les icônes",
"filter-default": "Icônes par défaut",
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}"
},
"basic_properties": {
"note_type": "Type de note",
@@ -1541,7 +1550,8 @@
"refresh-saved-search-results": "Rafraîchir les résultats de recherche enregistrée",
"create-child-note": "Créer une note enfant",
"unhoist": "Désactiver le focus",
"toggle-sidebar": "Basculer la barre latérale"
"toggle-sidebar": "Basculer la barre latérale",
"dropping-not-allowed": "Lâcher des notes à cet endroit n'est pas autorisé"
},
"title_bar_buttons": {
"window-on-top": "Épingler cette fenêtre au premier plan"
@@ -1549,10 +1559,19 @@
"note_detail": {
"could_not_find_typewidget": "Impossible de trouver typeWidget pour le type '{{type}}'",
"printing": "Impression en cours...",
"printing_pdf": "Export au format PDF en cours..."
"printing_pdf": "Export au format PDF en cours...",
"print_report_title": "Imprimer le rapport",
"print_report_collection_details_button": "Consulter les détails",
"print_report_collection_details_ignored_notes": "Notes ignorées"
},
"note_title": {
"placeholder": "saisir le titre de la note ici..."
"placeholder": "saisir le titre de la note ici...",
"created_on": "Créé le <Value />",
"last_modified": "Modifié le <Value />",
"note_type_switcher_label": "Basculer de {{type}} à :",
"note_type_switcher_others": "Autre type de note",
"note_type_switcher_templates": "Modèle",
"note_type_switcher_collection": "Collection"
},
"search_result": {
"no_notes_found": "Aucune note n'a été trouvée pour les paramètres de recherche donnés.",
@@ -1581,7 +1600,8 @@
},
"toc": {
"table_of_contents": "Table des matières",
"options": "Options"
"options": "Options",
"no_headings": "Pas d'en-tête."
},
"watched_file_update_status": {
"file_last_modified": "Le fichier <code class=\"file-path\"></code> a été modifié pour la dernière fois le <span class=\"file-last-modified\"></span>.",
@@ -1682,7 +1702,8 @@
"copy-link": "Copier le lien",
"paste": "Coller",
"paste-as-plain-text": "Coller comme texte brut",
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}"
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}",
"search_in_trilium": "Rechercher \"{{term}}\" dans Trilium"
},
"image_context_menu": {
"copy_reference_to_clipboard": "Copier la référence dans le presse-papiers",
@@ -1991,7 +2012,8 @@
"add-column": "Ajouter une colonne",
"add-column-placeholder": "Entrez le nom de la colonne...",
"edit-note-title": "Cliquez pour modifier le titre de la note",
"edit-column-title": "Cliquez pour modifier le titre de la colonne"
"edit-column-title": "Cliquez pour modifier le titre de la colonne",
"column-already-exists": "Cette colonne existe déjà dans le tableau."
},
"presentation_view": {
"edit-slide": "Modifier cette diapositive",
@@ -2075,7 +2097,8 @@
"button_title": "Exporter le diagramme au format PNG"
},
"svg": {
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG."
"export_to_png": "Le diagramme n'a pas pu être exporté au format PNG.",
"export_to_svg": "Le diagramme n'a pas pu être exporté en SVG."
},
"code_theme": {
"title": "Apparence",
@@ -2108,6 +2131,10 @@
},
"read-only-info": {
"read-only-note": "Vous consultez actuellement une note en lecture seule.",
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide."
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide.",
"edit-note": "Editer la note"
},
"calendar_view": {
"delete_note": "Effacer la note..."
}
}

View File

@@ -1,5 +1,50 @@
{
"about": {
"title": "ट्रिलियम नोट्स के बारें में"
"title": "ट्रिलियम नोट्स के बारें में",
"build_date": "निर्माण की तारीख:",
"app_version": "ऐप वर्ज़न:",
"db_version": "DB वर्ज़न:",
"build_revision": "बिल्ड रिविज़न:"
},
"toast": {
"widget-error": {
"title": "एक विजेट को इनिशियलाइज़ करने में विफल रहा"
},
"bundle-error": {
"title": "एक कस्टम स्क्रिप्ट लोड करने में विफल रहा"
},
"widget-list-error": {
"title": "सर्वर से विजेट्स की सूची प्राप्त करने में विफल"
},
"open-script-note": "स्क्रिप्ट नोट खोलें"
},
"update_available": {
"update_available": "उपलब्ध अद्यतन"
},
"code_buttons": {
"execute_button_title": "स्क्रिप्ट एक्सीक्यूट करें",
"trilium_api_docs_button_title": "ट्रिलियम एपीआई डॉक्स खोलें",
"save_to_note_button_title": "नोट में सेव करें"
},
"hide_floating_buttons_button": {
"button_title": "बटन छुपाएं"
},
"show_floating_buttons_button": {
"button_title": "बटन दिखाएं"
},
"add_link": {
"note": "नोट"
},
"bulk_actions": {
"other": "अन्य"
},
"clone_to": {
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
},
"confirm": {
"also_delete_note": "नोट भी डिलीट करें"
},
"delete_notes": {
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
}
}

View File

@@ -21,7 +21,13 @@
},
"bundle-error": {
"title": "Nem sikerült betölteni az egyéni szkriptet",
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
},
"widget-list-error": {
"title": "A Widget-ek letöltése sikertelen volt"
},
"widget-render-error": {
"title": "Nem sikerült renderelni a React widget-et"
}
},
"add_link": {

View File

@@ -16,13 +16,22 @@
},
"bundle-error": {
"title": "Non si è riusciti a caricare uno script personalizzato",
"message": "Lo script della nota con ID \"{{id}}\", dal titolo \"{{title}}\" non è stato inizializzato a causa di:\n\n{{message}}"
"message": "Impossibile eseguire lo script a causa di:\n\n{{message}}"
},
"widget-error": {
"title": "Impossibile inizializzare un widget",
"message-custom": "Il widget personalizzato dalla nota con ID “{{id}}”, intitolato “{{title}}”, non è stato possibile inizializzare a causa di:\n\n{{message}}",
"message-unknown": "Un widget sconosciuto non è stato inizializzato a causa di:\n\n{{message}}"
}
},
"widget-list-error": {
"title": "Impossibile ottenere l'elenco dei widget dal server"
},
"widget-render-error": {
"title": "Impossibile eseguire il rendering di un widget React personalizzato"
},
"widget-missing-parent": "Il widget personalizzato non ha la proprietà obbligatoria '{{property}}' definita.\n\nSe questo script deve essere eseguito senza un elemento dell'interfaccia utente, utilizzare invece '#run=frontendStartup'.",
"open-script-note": "Apri script note",
"scripting-error": "Errore script personalizzato: {{title}}"
},
"add_link": {
"add_link": "Aggiungi un collegamento",
@@ -893,7 +902,6 @@
"download_button": "Scarica",
"mime": "MIME: ",
"file_size": "Dimensione del file:",
"preview": "Anteprima:",
"preview_not_available": "L'anteprima non è disponibile per questo tipo di nota."
},
"sort_child_notes": {
@@ -1334,7 +1342,16 @@
"note_icon": {
"change_note_icon": "Cambia icona nota",
"search": "Ricerca:",
"reset-default": "Ripristina l'icona predefinita"
"reset-default": "Ripristina l'icona predefinita",
"search_placeholder_one": "Cerca {{number}} icona in {{count}} pacchetto",
"search_placeholder_many": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_other": "Cerca {{number}} icone in {{count}} pacchetti",
"search_placeholder_filtered": "Cerca {{number}} icone in {{name}}",
"filter": "Filtro",
"filter-none": "Tutte le icone",
"filter-default": "Icone predefinite",
"icon_tooltip": "{{name}}\nPacchetto icone: {{iconPack}}",
"no_results": "Nessuna icona trovata."
},
"basic_properties": {
"note_type": "Tipo di nota",
@@ -1792,7 +1809,7 @@
"will_be_deleted_in": "Questo allegato verrà eliminato automaticamente tra {{time}}",
"will_be_deleted_soon": "Questo allegato verrà eliminato automaticamente a breve",
"deletion_reason": ", perché l'allegato non è collegato al contenuto della nota. Per impedirne l'eliminazione, aggiungi nuovamente il collegamento all'allegato nel contenuto o converti l'allegato in nota.",
"role_and_size": "Ruolo: {{role}}, Dimensione: {{size}}",
"role_and_size": "Ruolo: {{role}}, dimensione: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Link all'allegato copiato negli appunti.",
"unrecognized_role": "Ruolo di allegato non riconosciuto '{{role}}'."
},
@@ -1878,7 +1895,11 @@
"create-child-note": "Crea nota figlio",
"unhoist": "Sganciare",
"toggle-sidebar": "Attiva/disattiva la barra laterale",
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -1886,7 +1907,13 @@
"note_detail": {
"could_not_find_typewidget": "Impossibile trovare typeWidget per il tipo '{{type}}'",
"printing": "Stampa in corso...",
"printing_pdf": "Esportazione in PDF in corso..."
"printing_pdf": "Esportazione in PDF in corso...",
"print_report_title": "Stampa rapporto",
"print_report_collection_content_one": "{{count}} la note nella raccolta non può essere stampata perché non è supportata o è protetta.",
"print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.",
"print_report_collection_details_button": "Vedi dettagli",
"print_report_collection_details_ignored_notes": "Note ignorate"
},
"note_title": {
"placeholder": "scrivi qui il titolo della nota...",
@@ -1896,7 +1923,8 @@
"note_type_switcher_others": "Altro tipo di nota",
"note_type_switcher_templates": "Modello",
"note_type_switcher_collection": "Collezione",
"edited_notes": "Note modificate"
"edited_notes": "Note modificate in questo giorno",
"promoted_attributes": "Attributi promossi"
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
@@ -2176,7 +2204,14 @@
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
"shared_copy_to_clipboard": "Copia link negli appunti",
"shared_open_in_browser": "Apri il link nel browser",
"shared_unshare": "Rimuovi condivisione"
"shared_unshare": "Rimuovi condivisione",
"save_status_saved": "Salvato",
"save_status_saving": "Salvataggio in corso...",
"save_status_unsaved": "Non salvato",
"save_status_error": "Salvataggio non riuscito",
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
},
"breadcrumb": {
"workspace_badge": "Area di lavoro",
@@ -2219,5 +2254,18 @@
"empty_button": "Nascondi il pannello",
"toggle": "Attiva/disattiva pannello destro",
"custom_widget_go_to_source": "Vai al codice sorgente"
},
"pdf": {
"attachments_one": "{{count}} allegato",
"attachments_many": "{{count}} allegati",
"attachments_other": "{{count}} allegati",
"layers_one": "{{count}} livello",
"layers_many": "{{count}} livelli",
"layers_other": "{{count}} livelli",
"pages_one": "{{count}} pagina",
"pages_many": "{{count}} pagine",
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
}
}

View File

@@ -153,7 +153,14 @@
"note_icon": {
"change_note_icon": "ノートアイコンの変更",
"search": "検索:",
"reset-default": "アイコンをデフォルトに戻す"
"reset-default": "アイコンをデフォルトに戻す",
"search_placeholder_other": "{{count}} 個のパックから {{number}} 個のアイコンを検索",
"search_placeholder_filtered": "{{name}} で {{number}} 個のアイコンを検索",
"filter": "フィルター",
"filter-none": "すべてのアイコン",
"filter-default": "デフォルトアイコン",
"icon_tooltip": "{{name}}\nアイコンパック: {{iconPack}}",
"no_results": "アイコンが見つかりません。"
},
"basic_properties": {
"note_type": "ノートタイプ",
@@ -436,7 +443,10 @@
"unhoist-note": "ノートのホイストを解除",
"edit-branch-prefix": "ブランチの接頭辞を編集",
"archive": "アーカイブ",
"unarchive": "アーカイブ解除"
"unarchive": "アーカイブ解除",
"open-in-a-new-window": "新しいウィンドウで開く",
"hide-subtree": "サブツリーを非表示",
"show-subtree": "サブツリーを表示"
},
"zen_mode": {
"button_exit": "禅モードを退出"
@@ -561,7 +571,8 @@
"expand_tooltip": "このコレクションの直下の子1階層下を展開します。その他のオプションについては、右側の矢印を押してください。",
"expand_first_level": "直下の子を展開",
"expand_nth_level": "{{depth}} 階層下まで展開",
"expand_all_levels": "すべての階層を展開"
"expand_all_levels": "すべての階層を展開",
"hide_child_notes": "ツリー内の子ノートを非表示"
},
"note_types": {
"geo-map": "ジオマップ",
@@ -647,7 +658,6 @@
"revision_deleted": "ノートの変更履歴は削除されました。",
"settings": "ノートの変更履歴の設定",
"file_size": "ファイルサイズ:",
"preview": "プレビュー:",
"preview_not_available": "このノートタイプではプレビューは利用できません。",
"diff_on": "差分を表示",
"diff_off": "内容を表示",
@@ -1238,7 +1248,15 @@
"saved-search-note-refreshed": "保存した検索ノートが更新されました。",
"refresh-saved-search-results": "保存した検索結果を更新",
"toggle-sidebar": "サイドバーを切り替え",
"dropping-not-allowed": "この場所にノートをドロップすることはできません。"
"dropping-not-allowed": "この場所にノートをドロップすることはできません。",
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
"shared-indicator-tooltip": "このノートは公開されています",
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}",
"subtree-hidden-tooltip_other": "{{count}} 個の子ノートがツリーで非表示になっています",
"subtree-hidden-moved-title": "{{title}} に追加されました",
"subtree-hidden-moved-description-collection": "このコレクションはツリー内の子ノートを非表示にします。",
"subtree-hidden-moved-description-other": "このノートのツリーでは子ノートは非表示になっています。"
},
"bulk_actions": {
"bulk_actions": "一括操作",
@@ -2129,7 +2147,7 @@
"will_be_deleted_in": "この添付ファイルは {{time}} 後に自動的に削除されます",
"will_be_deleted_soon": "この添付ファイルはすぐに自動的に削除されます",
"deletion_reason": "、添付ファイルがノートのコンテンツにリンクされていないためです。削除されないようにするには、添付ファイルのリンクをコンテンツに再度追加するか、添付ファイルをノートに変換してください。",
"role_and_size": "ロール: {{role}},サイズ: {{size}}",
"role_and_size": "ロール: {{role}},サイズ: {{size}}, MIME: {{- mimeType}}",
"link_copied": "添付ファイルのリンクをクリップボードにコピーしました。",
"unrecognized_role": "添付ファイルのロール「{{role}}」は認識されません。"
},
@@ -2186,7 +2204,14 @@
"execute_sql_description": "このノートは SQL ノートです。クリックすると SQL クエリが実行されます。",
"shared_copy_to_clipboard": "リンクをクリップボードにコピー",
"shared_open_in_browser": "ブラウザでリンクを開く",
"shared_unshare": "共有を削除"
"shared_unshare": "共有を削除",
"save_status_saved": "保存されました",
"save_status_saving": "保存中...",
"save_status_unsaved": "未保存",
"save_status_error": "保存に失敗しました",
"save_status_saving_tooltip": "変更を保存しています。",
"save_status_unsaved_tooltip": "未保存の変更があります。すぐに自動的に保存されます。",
"save_status_error_tooltip": "ノートの保存中にエラーが発生しました。可能であれば、ノートの内容を別の場所にコピーして、アプリケーションを再読み込みしてください。"
},
"status_bar": {
"language_title": "コンテンツの言語を変更",
@@ -2217,5 +2242,12 @@
},
"attributes_panel": {
"title": "ノート属性"
},
"pdf": {
"attachments_other": "{{count}} 添付ファイル",
"layers_other": "{{count}} 層",
"pages_other": "{{count}} ページ",
"pages_alt": "ページ {{pageNumber}}",
"pages_loading": "読み込み中..."
}
}

View File

@@ -1 +1,82 @@
{}
{
"about": {
"title": "Om Trilium Notes",
"app_version": "App versjon:",
"db_version": "DB versjon:",
"sync_version": "Synk versjon:",
"build_date": "Byggdato:",
"build_revision": "Bygg versjon:",
"data_directory": "Datamappe:",
"homepage": "Hjemmeside:"
},
"experimental_features": {
"new_layout_description": "Prøv det nye grensesnittet for et mer moderne utseende og forbedret brukervenlighet. Det må påregnes betydelige endringer i kommende versjoner."
},
"cpu_arch_warning": {
"recommendation": "For den beste brukeropplevelsen, vennligst last ned den tilpassede ARM64-versjonen av TriliumNext fra siden for utgivelser."
},
"zpetne_odkazy": {
"backlink_one": "{{count}} Tilbakelenke",
"backlink_other": "{{count}} Tilbakelenker"
},
"add_link": {
"note": "Notat"
},
"branch_prefix": {
"prefix": "Prefiks : ",
"save": "Lagre"
},
"bulk_actions": {
"labels": "Etiketter",
"relations": "Relasjoner",
"notes": "Notater",
"other": "Andre"
},
"confirm": {
"confirmation": "Bekreftelse",
"cancel": "Avbryt",
"ok": "OK"
},
"delete_notes": {
"close": "Lukk",
"cancel": "Avbryt",
"ok": "OK"
},
"export": {
"close": "Lukk",
"export": "Eksporter"
},
"note_type_chooser": {
"templates": "Maler"
},
"help": {
"title": "Hurtigveiledning",
"troubleshooting": "Feilsøking",
"other": "Andre"
},
"import": {
"options": "Alternativer",
"import": "Importer"
},
"include_note": {
"label_note": "Notat"
},
"prompt": {
"title": "Ledetekst",
"ok": "OK",
"defaultTitle": "Ledetekst"
},
"info": {
"closeButton": "Lukk",
"okButton": "OK"
},
"markdown_import": {
"import_button": "Importer"
},
"protected_session_password": {
"close_label": "Lukk"
},
"recent_changes": {
"undelete_link": "gjenopprett"
}
}

View File

@@ -12,7 +12,7 @@
"toast": {
"critical-error": {
"title": "Kritische Error",
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
"message": "Een kritieke fout heeft plaatsgevonden waardoor de applicatie niet kon opstarten:\n\n{{message}}\n\nDit is waarschijnlijk veroorzaakt door een onverwachte fout in een script. Probeer de applicatie op te starten in veilige modus en het probleem op te lossen."
},
"widget-error": {
"title": "Starten widget mislukt",
@@ -22,7 +22,16 @@
"bundle-error": {
"title": "Custom script laden mislukt",
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
}
},
"scripting-error": "Error met script: {{title}}",
"widget-list-error": {
"title": "Kon geen lijst met widgets ophalen van de server"
},
"widget-render-error": {
"title": "React-widget kon niet geladen worden"
},
"widget-missing-parent": "Widget heeft niet het verplichte '{{property}}'-veld gedefinieerd.\n\nAls dit script is bedoeld om zonder interface te draaien, gebruik dan in plaats daarvan '#run=frontendStartup'.",
"open-script-note": "Open scriptnotitie"
},
"add_link": {
"add_link": "Voeg link toe",
@@ -41,7 +50,8 @@
"help_on_tree_prefix": "Help bij boomvoorvoegsel",
"prefix": "Voorvoegsel: ",
"edit_branch_prefix_multiple": "Bewerk zijtakvoorvoegsel voor {{count}} zijtakken",
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen."
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen.",
"affected_branches": "Aangetaste takken ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Bulk acties",
@@ -54,7 +64,8 @@
"labels": "Labels",
"relations": "Relaties",
"notes": "Notities",
"other": "Andere"
"other": "Andere",
"include_descendants": "Tel afstammelingen van de geselecteerde notities mee"
},
"calendar": {
"april": "April",
@@ -78,5 +89,35 @@
},
"show_toc_widget_button": {
"show_toc": "Laat Inhoudsopgave zien"
},
"status_bar": {
"note_paths_one": "{{count}} pad",
"note_paths_other": "{{count}} paden",
"note_paths_title": "Notitiepaden",
"code_note_switcher": "Verander de taalmodus"
},
"attributes_panel": {
"title": "Notitie-attributen"
},
"right_pane": {
"empty_message": "Geen informatie voor deze notitie",
"empty_button": "Verberg dit paneel",
"toggle": "Schakel rechterpaneel in/uit",
"custom_widget_go_to_source": "Go naar de broncode"
},
"pdf": {
"attachments_one": "{{count}} bijlage",
"attachments_other": "{{count}} bijlagen",
"layers_one": "{{count}} laag",
"layers_other": "{{count}} lagen",
"pages_one": "{{count}} pagina",
"pages_other": "{{count}} pagina's",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Laden..."
},
"clone_to": {
"clone_notes_to": "Kloon de notities naar...",
"help_on_links": "Hulp op links",
"notes_to_clone": "Notities om te klonen"
}
}

View File

@@ -959,7 +959,6 @@
"download_button": "Pobierz",
"mime": "MIME: ",
"file_size": "Rozmiar pliku:",
"preview": "Podgląd:",
"preview_not_available": "Podgląd nie jest dostępny dla tego typu notatki."
},
"sort_child_notes": {

View File

@@ -22,7 +22,16 @@
"bundle-error": {
"title": "Falha para carregar o script customizado",
"message": "O script da nota com ID \"{{id}}\", intitulada \"{{title}}\", não pôde ser executado devido a:\n\n{{message}}"
}
},
"widget-list-error": {
"title": "Falha ao obter a lista de widgets do servidor"
},
"scripting-error": "Erro do script específicado: {{title}}",
"open-script-note": "Abrir script da nota",
"widget-render-error": {
"title": "Falha do renderizar um widget React personalizado"
},
"widget-missing-parent": "Widget adaptado não tem a propriedade '{{property}}' mandatória definida.\n\nSe este script é para ser executado sem um element de UI, usar '#run=frontendStartup'."
},
"add_link": {
"add_link": "Adicionar ligação",
@@ -39,7 +48,10 @@
"help_on_tree_prefix": "Ajuda sobre o prefixo da árvore de notas",
"prefix": "Prefixo: ",
"save": "Gravar",
"branch_prefix_saved": "O prefixo de ramificação foi gravado."
"branch_prefix_saved": "O prefixo de ramificação foi gravado.",
"edit_branch_prefix_multiple": "Editar prefixo para {{count}} branches",
"branch_prefix_saved_multiple": "Prefixo dos branches foi editado para {{count}} branches.",
"affected_branches": "Alterados ({{count}}) branches:"
},
"bulk_actions": {
"bulk_actions": "Ações em massa",
@@ -104,7 +116,8 @@
"export_status": "Estado da exportação",
"export_in_progress": "Exportação em andamento: {{progressCount}}",
"export_finished_successfully": "Exportação concluída com sucesso.",
"format_pdf": "PDF para impressão ou compartilhamento."
"format_pdf": "PDF para impressão ou compartilhamento.",
"share-format": "HTML para publicação web - usa o mesmo tema que é usado para notas partilhadas, mas pode ser publicado como um site estatico."
},
"help": {
"title": "Folha de Dicas",
@@ -158,7 +171,8 @@
"showSQLConsole": "mostrar console SQL",
"other": "Outros",
"quickSearch": "focar no campo de pesquisa rápida",
"inPageSearch": "pesquisa na página"
"inPageSearch": "pesquisa na página",
"editShortcuts": "Editar atalhos do teclado"
},
"import": {
"importIntoNote": "Importar para a nota",
@@ -184,7 +198,8 @@
},
"import-status": "Estado da importação",
"in-progress": "Importação em andamento: {{progress}}",
"successful": "Importação concluída com sucesso."
"successful": "Importação concluída com sucesso.",
"importZipRecommendation": "Quando a importar ficheiro ZIP, a hierarquia de notas vai reflectir a estrutura da sub directoria dentro do ficheiro."
},
"include_note": {
"dialog_title": "Incluir nota",
@@ -199,7 +214,8 @@
"info": {
"modalTitle": "Mensagem informativa",
"closeButton": "Fechar",
"okButton": "OK"
"okButton": "OK",
"copy_to_clipboard": "Copiar para a área de transferência"
},
"jump_to_note": {
"search_placeholder": "Pesquise uma nota pelo nome ou digite > para comandos...",
@@ -274,8 +290,12 @@
"download_button": "Descarregar",
"mime": "MIME: ",
"file_size": "Tamanho do ficheiro:",
"preview": "Visualizar:",
"preview_not_available": "A visualização não está disponível para este tipo de nota."
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
"diff_on": "Mostrar diferenças",
"diff_off": "Mostrar conteúdos",
"diff_on_hint": "Carregar para mostrar diferenças da fonte da nota",
"diff_off_hint": "Carregar para mostrar conteúdos da nota",
"diff_not_available": "Diferenças não disponível."
},
"sort_child_notes": {
"sort_children_by": "Ordenar notas filhas por...",
@@ -586,7 +606,18 @@
"september": "Setembro",
"october": "Outubro",
"november": "Novembro",
"december": "Dezembro"
"december": "Dezembro",
"week": "Semana",
"week_previous": "Semana anterior",
"week_next": "Próxima semana",
"month": "Mês",
"month_previous": "Mês anterior",
"month_next": "Próximo mês",
"year": "Ano",
"year_previous": "Ano anterior",
"year_next": "Próximo ano",
"list": "Lista",
"today": "Hoje"
},
"close_pane_button": {
"close_this_pane": "Fechar este painel"
@@ -629,7 +660,9 @@
"about": "Sobre o Trilium Notes",
"logout": "Sair",
"show-cheatsheet": "Exibir Cheatsheet",
"toggle-zen-mode": "Modo Zen"
"toggle-zen-mode": "Modo Zen",
"new-version-available": "Nova actualização disponível",
"download-update": "Obter versão {{latestVersion}}"
},
"zen_mode": {
"button_exit": "Sair do Modo Zen"
@@ -667,7 +700,14 @@
"convert_into_attachment_failed": "A conversão da nota '{{title}}' falhou.",
"convert_into_attachment_successful": "A nota '{{title}}' foi convertida para anexo.",
"convert_into_attachment_prompt": "Tem certeza que quer converter a nota '{{title}}' num anexo da nota pai?",
"print_pdf": "Exportar como PDF…"
"print_pdf": "Exportar como PDF…",
"open_note_on_server": "Abrir nota no servidor",
"export_as_image": "Exportar como imagem",
"note_map": "Mapa de notas",
"advanced": "Avançadas",
"view_revisions": "Revisões da nota...",
"export_as_image_svg": "SVG (vectorial)",
"export_as_image_png": "PNG (matricial)"
},
"onclick_button": {
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
@@ -713,19 +753,29 @@
"zpetne_odkazy": {
"relation": "relação",
"backlink_one": "{{count}} Ligação Reversa",
"backlink_many": "",
"backlink_many": "{{count}} Ligações Reversas",
"backlink_other": "{{count}} Ligações Reversas"
},
"mobile_detail_menu": {
"insert_child_note": "Inserir nota filha",
"delete_this_note": "Apagar esta nota",
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '",
"error_unrecognized_command": "Comando não reconhecido {{command}}"
"error_unrecognized_command": "Comando não reconhecido {{command}}",
"note_revisions": "Revisões da nota"
},
"note_icon": {
"change_note_icon": "Alterar ícone da nota",
"search": "Pesquisa:",
"reset-default": "Redefinir para o ícone padrão"
"reset-default": "Redefinir para o ícone padrão",
"filter": "Filtrar",
"filter-none": "Todos os icons",
"filter-default": "Icons default",
"no_results": "Não foram encontrados icons.",
"search_placeholder_filtered": "Procurar {{number}} icons no {{name}}",
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}",
"search_placeholder_one": "Procurar {{number}} icon nos {{count}} pacotes",
"search_placeholder_many": "Procurar {{number}} icons em {{count}} pacotes",
"search_placeholder_other": "Procurar {{number}} icons nos {{count}} pacotes"
},
"basic_properties": {
"note_type": "Tipo da nota",
@@ -746,7 +796,14 @@
"calendar": "Calendário",
"table": "Tabela",
"geo-map": "Mapa geográfico",
"board": "Quadro"
"board": "Quadro",
"expand_first_level": "Expandir descendentes directos",
"presentation": "Apresentação",
"expand_nth_level": "Expandir {{depth}} níveis",
"expand_all_levels": "Expandir todos os níveis",
"include_archived_notes": "Mostrar notas arquivadas",
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita.",
"hide_child_notes": "Esconder notas descendentes na árvore"
},
"edited_notes": {
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
@@ -779,7 +836,8 @@
},
"inherited_attribute_list": {
"title": "Atributos Herdados",
"no_inherited_attributes": "Nenhum atributo herdado."
"no_inherited_attributes": "Nenhum atributo herdado.",
"none": "Nenhum"
},
"note_info_widget": {
"note_id": "ID da Nota",
@@ -790,7 +848,9 @@
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo das suas revisões de nota.",
"calculate": "calcular",
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
"title": "Informações da nota"
"title": "Informações da nota",
"mime": "Tipo MIME",
"show_similar_notes": "Mostrar notas semelhantes"
},
"note_map": {
"open_full": "Expandir completamente",
@@ -853,7 +913,8 @@
"search_parameters": "Parâmetros de Pesquisa",
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
"search_note_saved": "Nota de pesquisa foi gravada em {{- notePathTitle}}",
"actions_executed": "As ações foram executadas."
"actions_executed": "As ações foram executadas.",
"view_options": "Ver opções:"
},
"similar_notes": {
"title": "Notas Similares",
@@ -947,14 +1008,22 @@
"no_attachments": "Esta nota não possuí anexos."
},
"book": {
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores."
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores.",
"drag_locked_title": "Bloqueado para edição",
"drag_locked_message": "Arrastar não permitida pois a coleção está bloqueada para edição."
},
"editable_code": {
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
},
"editable_text": {
"placeholder": "Digite o conteúdo da sua nota aqui…",
"auto-detect-language": "Detetado automaticamente"
"auto-detect-language": "Detetado automaticamente",
"editor_crashed_title": "O editor de texto quebrou",
"editor_crashed_details_button": "Ver mais detalhes...",
"editor_crashed_details_title": "Informação técnica",
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo.",
"editor_crashed_content": "O seu conteudo foi recuperado com sucesso, mas alguns das alterações mais recentes podem não ter sido gravadas.",
"keeps-crashing": "Componente de edição a rebentar continuamente. Por favor tentar reiniciar Trilium. Se o problema persistir, considere abrir um bug report."
},
"empty": {
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
@@ -1082,7 +1151,8 @@
"title": "Largura do Conteúdo",
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
"max_width_label": "Largura máxima do conteúdo",
"max_width_unit": "pixels"
"max_width_unit": "pixels",
"centerContent": "Manter conteúdo centrado"
},
"native_title_bar": {
"title": "Barra de Título Nativa (requer recarregar a app)",
@@ -1114,7 +1184,9 @@
"title": "Desempenho",
"enable-motion": "Ativar transições e animações",
"enable-shadows": "Ativar sombras",
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis"
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis",
"enable-smooth-scroll": "Activar deslocamento suave",
"app-restart-required": "(é necessário reiniciar a aplicação para aplicar as alterações)"
},
"ai_llm": {
"not_started": "Não iniciado",
@@ -1273,7 +1345,10 @@
"title": "Editor"
},
"code_mime_types": {
"title": "Tipos MIME disponíveis no dropdown"
"title": "Tipos MIME disponíveis no dropdown",
"tooltip_syntax_highlighting": "Destaque de sintaxe",
"tooltip_code_block_syntax": "Blocos de código nas notas de texto",
"tooltip_code_note_syntax": "Notas de código"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Atribuições de teclas do Vim",
@@ -1393,7 +1468,13 @@
"min-days-in-first-week": "Mínimo de dias da primeira semana",
"first-week-info": "Primeira semana que contenha a primeira Quinta-feira do ano é baseado na <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
"first-week-warning": "Alterar as opções de primeira semana pode causar duplicidade nas Notas Semanais existentes e estas Notas não serão atualizadas de acordo.",
"formatting-locale": "Formato de data e número"
"formatting-locale": "Formato de data e número",
"tuesday": "Terça-feira",
"wednesday": "Quarta-feira",
"thursday": "Quinta-feira",
"friday": "Sexta-feira",
"saturday": "Sábado",
"formatting-locale-auto": "Baseado na linguagem da aplicação"
},
"backup": {
"automatic_backup": "Backup automático",
@@ -1486,7 +1567,8 @@
"oauth_description_warning": "Para ativar o OAuth/OpenID, precisa definir a URL base do OAuth/OpenID, o client ID e o client secret no ficheiro config.ini e reiniciar a aplicação. Se quiser configurar via variáveis de ambiente, defina TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID e TRILIUM_OAUTH_CLIENT_SECRET.",
"oauth_user_account": "Conta do Utilizador: ",
"oauth_user_email": "E-mail do Utilizador: ",
"oauth_user_not_logged_in": "Não está logado!"
"oauth_user_not_logged_in": "Não está logado!",
"oauth_missing_vars": "Configurações em falta: {{-variables}}"
},
"shortcuts": {
"keyboard_shortcuts": "Atalhos de Teclado",
@@ -1586,7 +1668,12 @@
"apply-bulk-actions": "Aplicar ações em massa",
"converted-to-attachments": "{{count}} notas foram convertidas em anexos.",
"convert-to-attachment-confirm": "Tem certeza que deseja converter as notas selecionadas em anexos das suas notas-pai?",
"open-in-popup": "Edição rápida"
"open-in-popup": "Edição rápida",
"open-in-a-new-window": "Abrir numa nova janela",
"archive": "Arquivar",
"unarchive": "Retirar do arquivo",
"hide-subtree": "Esconder sub-árvore",
"show-subtree": "Mostrar sub-árvore"
},
"shared_info": {
"shared_publicly": "Esta nota é partilhada publicamente em {{- link}}.",
@@ -1647,7 +1734,13 @@
},
"highlights_list_2": {
"title": "Lista de Destaques",
"options": "Opções"
"options": "Opções",
"no_highlights": "Sem destaques encontrados.",
"menu_configure": "Configurar lista de destaques...",
"modal_title": "Configurar list de destaques",
"title_with_count_one": "{{count}} destaque",
"title_with_count_many": "{{count}} destaques",
"title_with_count_other": "{{count}} destaques"
},
"quick-search": {
"placeholder": "Pesquisa rápida",
@@ -1670,16 +1763,43 @@
"refresh-saved-search-results": "Atualizar resultados de pesquisa gravados",
"create-child-note": "Criar nota filha",
"unhoist": "Desafixar",
"toggle-sidebar": "Alternar barra lateral"
"toggle-sidebar": "Alternar barra lateral",
"dropping-not-allowed": "Largar notas nesta localização não é permitida",
"clone-indicator-tooltip": "Esta nota tem {{- count}} ascendentes: {{- parents}}",
"shared-indicator-tooltip": "Esta nota está partilhada publicamente",
"shared-indicator-tooltip-with-url": "Esta nota está partilhada publicamente em: {{- url}}",
"subtree-hidden-moved-title": "Adicionar ao {{title}}",
"subtree-hidden-moved-description-collection": "Esta colecção esconde as notas descendentes na árvore.",
"subtree-hidden-moved-description-other": "Notas descendentes estão escondidades na árvore para esta nota.",
"subtree-hidden-tooltip_one": "{{count}} nota descendentes escondidas da árvore",
"subtree-hidden-tooltip_many": "{{count}} notas descendentes escondidas da árvore",
"subtree-hidden-tooltip_other": "{{count}} notas descendentes escondidas da árvore",
"clone-indicator-tooltip-single": "Esta nota está clonada (1 additional parent: {{- parent}})"
},
"title_bar_buttons": {
"window-on-top": "Manter Janela no Topo"
},
"note_detail": {
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'"
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'",
"print_report_collection_details_button": "Ver detalhes",
"printing": "Impressão em progresso...",
"printing_pdf": "Exportação PDF em progresso...",
"print_report_title": "Imprimir relatório",
"print_report_collection_details_ignored_notes": "Ignorar notas",
"print_report_collection_content_one": "{{count}} nota na colecção não pode ser impressa porque não é suportado ou está protegida.",
"print_report_collection_content_many": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas.",
"print_report_collection_content_other": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas."
},
"note_title": {
"placeholder": "digite o título da nota aqui..."
"placeholder": "digite o título da nota aqui...",
"promoted_attributes": "Atributos destacados",
"created_on": "Criado em <Value />",
"last_modified": "Modificado em <Value />",
"note_type_switcher_label": "Alterar de {{type}} para:",
"note_type_switcher_others": "Outro tipo de nota",
"note_type_switcher_templates": "Template",
"note_type_switcher_collection": "Colecção",
"edited_notes": "Notas editadas neste dia"
},
"search_result": {
"no_notes_found": "Nenhuma nota encontrada para os parâmetros de pesquisa digitados.",
@@ -1708,7 +1828,8 @@
},
"toc": {
"table_of_contents": "Tabela de Conteúdos",
"options": "Opções"
"options": "Opções",
"no_headings": "Sem cabeçalhos."
},
"watched_file_update_status": {
"file_last_modified": "O ficheiro <code class=\"file-path\"></code> foi modificado pela última vez em <span class=\"file-last-modified\"></span>.",
@@ -1751,7 +1872,9 @@
"ws": {
"sync-check-failed": "A verificação de sincronização falhou!",
"consistency-checks-failed": "A verificação de consistência falhou! Veja os logs para pormenores.",
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console."
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console.",
"lost-websocket-connection-title": "Perdida conexão com o servidor",
"lost-websocket-connection-message": "Verifique a configuração da proxy inversa (e.g. nginx ou Apache) para assegurar conexões WebSocket estão permitidas e não bloqueadas."
},
"hoisted_note": {
"confirm_unhoisting": "A nota solicitada '{{requestedNote}}' está fora da árvore da nota fixada '{{hoistedNote}}' e precisa desafixar para aceder a nota. Quer prosseguir e desafixar?"
@@ -1807,7 +1930,8 @@
"copy-link": "Copiar ligação",
"paste": "Colar",
"paste-as-plain-text": "Colar como texto sem formatação",
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}"
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}",
"search_in_trilium": "A procurar \"{{term}}\" no Trilium"
},
"image_context_menu": {
"copy_reference_to_clipboard": "Copiar referência para a área de transferência",
@@ -1817,7 +1941,8 @@
"open_note_in_new_tab": "Abrir nota em nova guia",
"open_note_in_new_split": "Abrir nota em nova divisão",
"open_note_in_new_window": "Abrir nota em nova janela",
"open_note_in_popup": "Edição rápida"
"open_note_in_popup": "Edição rápida",
"open_note_in_other_split": "Abrir nota noutro separador"
},
"electron_integration": {
"desktop-application": "Aplicação Desktop",
@@ -1825,7 +1950,8 @@
"native-title-bar-description": "Para Windows e macOS, manter a barra de título nativa desativada faz a aplicação parecer mais compacta. No Linux, manter a barra de título nativa ativada faz a aplicação se integrar melhor com o restante do sistema.",
"background-effects": "Ativar efeitos de fundo (apenas Windows 11)",
"restart-app-button": "Reiniciar a aplicação para ver as alterações",
"zoom-factor": "Fator de Zoom"
"zoom-factor": "Fator de Zoom",
"background-effects-description": "O Mica adiciona um desfoque, fundo estiloso as janelas da aplicação, criando uma profundidade e aspecto moderno. \"Barra de titulo nativa\" deve estar inactiva."
},
"note_autocomplete": {
"search-for": "Pesquisar por \"{{term}}\"",
@@ -1885,7 +2011,8 @@
},
"note_language": {
"not_set": "Não atribuído",
"configure-languages": "Configurar idiomas..."
"configure-languages": "Configurar idiomas...",
"help-on-languages": "Ajuda nas linguagens de conteúdos..."
},
"content_language": {
"title": "Idiomas do conteúdo",
@@ -1903,7 +2030,8 @@
"button_title": "Exportar diagrama como PNG"
},
"svg": {
"export_to_png": "O diagrama não pôde ser exportado como PNG."
"export_to_png": "O diagrama não pôde ser exportado como PNG.",
"export_to_svg": "O diagrama não pode ser exportado para SVG."
},
"code_theme": {
"title": "Aparência",
@@ -1922,7 +2050,11 @@
"editorfeatures": {
"title": "Recursos",
"emoji_completion_enabled": "Ativar auto-completar de Emoji",
"note_completion_enabled": "Ativar auto-completar de notas"
"note_completion_enabled": "Ativar auto-completar de notas",
"emoji_completion_description": "Se activo, emojis podem ser facilmente inseridos em texto ao pressionar `:`, seguido do nome de um emoji.",
"note_completion_description": "Se activo, links para notas podem ser criadas ao escrever `@` seguido do titulo de uma nota.",
"slash_commands_enabled": "Activar comentários simples",
"slash_commands_description": "Se activo, editar comandos como inserir quebras de linha ou cabeçalhos podem ser activado/inactivado ao escrever `/`."
},
"table_view": {
"new-row": "Nova linha",
@@ -1964,7 +2096,16 @@
"delete-column": "Apagar coluna",
"delete-column-confirmation": "Tem certeza que deseja apagar esta coluna? O atributo correspondente também será apagado de todas as notas abaixo desta coluna.",
"new-item": "Novo elemento",
"add-column": "Adicionar Coluna"
"add-column": "Adicionar Coluna",
"delete-note": "Apagar nota...",
"remove-from-board": "Remover do quadro",
"archive-note": "Arquivar nota",
"new-item-placeholder": "Inserir titulo da nota...",
"add-column-placeholder": "Inserir nome da coluna...",
"edit-note-title": "Clicar para editar o titulo da nota",
"unarchive-note": "Remover nota do arquivo",
"edit-column-title": "Click para editar titulo da coluna",
"column-already-exists": "Esta coluna já existe no quadro."
},
"command_palette": {
"tree-action-name": "Árvore: {{name}}",
@@ -1995,16 +2136,146 @@
"background_effects_title": "Efeitos de fundo estão estáveis agora",
"background_effects_message": "Em dispositivos Windows, efeitos de fundo estão estáveis agora. Os efeitos de fundo adicionam um toque de cor à interface do utilizador borrando o plano de fundo atrás dela. Esta técnica também é usada noutras aplicações como o Windows Explorer.",
"background_effects_button": "Ativar os efeitos de fundo",
"dismiss": "Dispensar"
"dismiss": "Dispensar",
"new_layout_title": "Novo titulo do layout",
"new_layout_button": "Mais informação",
"new_layout_message": "Estamos a introduzir um layout modernizado para o Trilium. A faixa foi removida e está integrada na interface principal, com uma nota barra de estado e secções expansíveis (como as propriedades próprias) a tomar papéis principais.\n\nO novo layout está activo por defeito, e pode ser temporáriamente disabilidade em Opções → Aparência."
},
"settings": {
"related_settings": "Configurações relacionadas"
},
"settings_appearance": {
"related_code_blocks": "Esquema de cores para blocos de código em notas de texto",
"related_code_notes": "Esquema de cores para notas de código"
"related_code_notes": "Esquema de cores para notas de código",
"ui": "Interface do utilizador",
"ui_old_layout": "Layout antigo",
"ui_new_layout": "Nova aparência"
},
"units": {
"percentage": "%"
},
"experimental_features": {
"title": "Opções experimentais",
"new_layout_name": "Novo layout",
"new_layout_description": "Experimente o novo layout para um aspecto moderno e melhor estabilidade. Sujeito a grandes alterações nas próximas publicações.",
"disclaimer": "Estas opções são experimentais e podem causar instabilidade. Usar com cuidado."
},
"read-only-info": {
"read-only-note": "Actualmente a ver em modo de leitura.",
"edit-note": "Editar nota",
"auto-read-only-note": "Esta nota está a ser mostrada em modo de leitura para um carregamento mais rápido."
},
"presentation_view": {
"edit-slide": "Editar este slide",
"start-presentation": "Iniciar apresentação",
"slide-overview": "Alternar visão geral dos slides"
},
"calendar_view": {
"delete_note": "Apagar nota..."
},
"pagination": {
"page_title": "Página {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} notas"
},
"collections": {
"rendering_error": "Sem possíbilidade de mostrar conteúdos devido a um erro."
},
"note-color": {
"clear-color": "Remover cor da nota",
"set-color": "Atribuir cor da nota",
"set-custom-color": "Afectar cor personalizada da nota"
},
"popup-editor": {
"maximize": "Alterar para editor completo"
},
"server": {
"unknown_http_error_title": "Erro na comunicação com servidor",
"unknown_http_error_content": "Código de estado: {{statusCode}}\nURL: {{method}} {{url}}\nMessagem: {{message}}",
"traefik_blocks_requests": "Se está a usar o Traefik, este introduz uma alteração que afecta a comunicação com o servidor."
},
"tab_history_navigation_buttons": {
"go-back": "Ir para a nota anterior",
"go-forward": "Ir para nota seguinte"
},
"breadcrumb": {
"hoisted_badge": "Içado",
"workspace_badge": "Área de trabalho",
"scroll_to_top_title": "Saltar para o início da nota",
"create_new_note": "Criar nova nota descendente",
"empty_hide_archived_notes": "Esconder notas arquivadas",
"hoisted_badge_title": "Retirar de içado"
},
"breadcrumb_badges": {
"read_only_explicit": "Modo de leitura",
"read_only_auto": "Modo de leitura automático",
"read_only_temporarily_disabled": "Editável temporáriamente",
"read_only_auto_description": "Esta nota foi automaticamente colocada em modo de leitura por razões de performance. Este limite automatico é ajustável nas configurações.\n\nClicar para editar temporáriamente.",
"read_only_temporarily_disabled_description": "Esta nota está editável, mas normalmente está em modo de leitura. A nova vai regressar para mode de leitura assim que navegar para outra nota.\n\nClicar para reactivar o modo de leitura.",
"read_only_explicit_description": "Esta nota foi manualmente colocada em modo de leitura.\nClicar para editar temporáriamente.",
"shared_publicly": "Partilhado publicamente",
"shared_locally": "Partilhado localmente",
"shared_copy_to_clipboard": "Copiar link para a área de transferência",
"shared_open_in_browser": "Abrir link no browser",
"shared_unshare": "Remover partilha",
"clipped_note_description": "Esta nota foi retirar do {{url}}.\n\nClicar para navegar no código fonte da página.",
"clipped_note": "Web clipe",
"execute_script": "Correr script",
"execute_script_description": "Esta nota é uma nota de script. Clicar para executar o script.",
"execute_sql": "Correr SQL",
"execute_sql_description": "Esta nota é uma nota de SQL. Clicar para executar script SQL.",
"save_status_saved": "Guardar",
"save_status_saving": "A guardar...",
"save_status_unsaved": "Não gravado",
"save_status_error": "Gravar falhou",
"save_status_saving_tooltip": "Alterações estão a ser guardadas",
"save_status_unsaved_tooltip": "Existem alterações não guardadas. Serão guardadas automaticamente em breve.",
"save_status_error_tooltip": "Ocorreu um erro ao guardar a nota. Se possível, tente copiar os conteúdos da nota para outro local e reiniciar a aplicação."
},
"status_bar": {
"language_title": "Alterar lingua do conteúdo",
"note_info_title": "Ver informação da nota (e.g., datas, tamanho da nota)",
"backlinks_one": "{{count}} backlink",
"backlinks_many": "{{count}} backlinks",
"backlinks_other": "{{count}} backlinks",
"backlinks_title_one": "Ver backlink",
"backlinks_title_many": "Ver backlinks",
"backlinks_title_other": "Ver backlinks",
"attachments_one": "{{count}} anexo",
"attachments_many": "{{count}} anexos",
"attachments_other": "{{count}} anexos",
"attachments_title_one": "Ver anexo num novo separador",
"attachments_title_many": "Ver anexos num novo separador",
"attachments_title_other": "Ver anexos num novo separador",
"attributes_one": "{{count}} atributo",
"attributes_many": "{{count}} atributos",
"attributes_other": "{{count}} atributos",
"attributes_title": "Atributos próprios e herdados",
"note_paths_one": "{{count}} caminho",
"note_paths_many": "{{count}} caminhos",
"note_paths_other": "{{count}} caminhos",
"note_paths_title": "Caminhos da nota",
"code_note_switcher": "Alterar modo de linguagem"
},
"attributes_panel": {
"title": "Atributos da nota"
},
"right_pane": {
"empty_message": "Nada para mostrar nesta nota",
"empty_button": "Esconder painél",
"toggle": "Alterar painel direito",
"custom_widget_go_to_source": "Ir para código fonte"
},
"pdf": {
"attachments_one": "{{count}} anexo pdf",
"attachments_many": "{{count}} anexos pdf",
"attachments_other": "{{count}} anexos pdf",
"layers_one": "{{count}} camada",
"layers_many": "{{count}} camadas",
"layers_other": "{{count}} camadas",
"pages_one": "{{count}} página",
"pages_many": "{{count}} páginas",
"pages_other": "{{count}} páginas",
"pages_alt": "Página {{pageNumber}}",
"pages_loading": "A carregar..."
}
}

View File

@@ -439,7 +439,6 @@
"download_button": "Download",
"mime": "MIME: ",
"file_size": "Tamanho do arquivo:",
"preview": "Visualizar:",
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
"diff_on": "Exibir diferença",
"diff_off": "Exibir conteúdo",

View File

@@ -1103,7 +1103,6 @@
"mime": "MIME: ",
"no_revisions": "Nu există încă nicio revizie pentru această notiță...",
"note_revisions": "Revizii ale notiței",
"preview": "Previzualizare:",
"preview_not_available": "Nu este disponibilă o previzualizare pentru acest tip de notiță.",
"restore_button": "Restaurează",
"revision_deleted": "Revizia notiței a fost ștearsă.",

View File

@@ -387,7 +387,6 @@
"revision_deleted": "Версия заметки была удалена.",
"download_button": "Скачать",
"file_size": "Размер файла:",
"preview": "Предпросмотр:",
"preview_not_available": "Предпосмотр недоступен для заметки этого типа.",
"mime": "MIME: ",
"settings": "Настройка версионирования заметок",
@@ -1012,7 +1011,16 @@
"note_icon": {
"search": "Поиск:",
"change_note_icon": "Изменить иконку заметки",
"reset-default": "Сбросить к значку по умолчанию"
"reset-default": "Сбросить к значку по умолчанию",
"no_results": "Иконки не найдены.",
"icon_tooltip": "{{name}}\nНабор иконок: {{iconPack}}",
"filter-default": "Иконки по-умолчанию",
"filter-none": "Все иконки",
"filter": "Фильтр",
"search_placeholder_filtered": "Поиск {{number}} иконок в {{name}}",
"search_placeholder_one": "Поиск {{number}} иконки среди {{count}} наборов",
"search_placeholder_few": "Поиск {{number}} иконок среди {{count}} наборов",
"search_placeholder_many": "Поиск {{number}} иконок среди {{count}} наборов"
},
"basic_properties": {
"editable": "Изменяемое",
@@ -2025,7 +2033,7 @@
"lost-websocket-connection-message": "Проверьте конфигурацию обратного прокси (например, nginx или Apache), чтобы убедиться, что соединения WebSocket должным образом разрешены и не заблокированы."
},
"attachment_detail_2": {
"role_and_size": "Роль: {{role}}, Размер: {{size}}",
"role_and_size": "Роль: {{role}}, размер: {{size}}, MIME: {{- mimeType}}",
"unrecognized_role": "Нераспознанная роль вложения '{{role}}'.",
"link_copied": "Ссылка на вложение скопирована в буфер обмена.",
"will_be_deleted_soon": "Это вложение скоро будет автоматически удалено",

View File

@@ -271,7 +271,6 @@
"download_button": "Preuzmi",
"mime": "MIME: ",
"file_size": "Veličina datoteke:",
"preview": "Pregled:",
"preview_not_available": "Pregled nije dostupan za ovaj tip beleške."
},
"sort_child_notes": {

View File

@@ -29,8 +29,9 @@
"widget-render-error": {
"title": "無法渲染自訂 React 元件"
},
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。",
"open-script-note": "打開腳本筆記"
"widget-missing-parent": "自訂元件未定義強制性的 \"{{property}}\" 屬性。\n\n若此腳本需在無 UI 的情況下執行,請改用 \"#run=frontendStartup\"。",
"open-script-note": "打開腳本筆記",
"scripting-error": "自訂腳本錯誤:{{title}}"
},
"add_link": {
"add_link": "新增連結",
@@ -287,7 +288,6 @@
"download_button": "下載",
"mime": "MIME類型 ",
"file_size": "檔案大小:",
"preview": "預覽:",
"preview_not_available": "無法預覽此類型的筆記。",
"restore_button": "還原",
"delete_button": "刪除",
@@ -762,7 +762,15 @@
"note_icon": {
"change_note_icon": "更改筆記圖標",
"search": "搜尋:",
"reset-default": "重置為預設圖標"
"reset-default": "重置為預設圖標",
"search_placeholder_one": "在 {{count}} 個圖示包中搜尋 {{number}} 個圖示",
"search_placeholder_other": "",
"search_placeholder_filtered": "在 {{name}} 中搜尋 {{number}} 個圖示",
"filter": "篩選",
"filter-none": "所有圖示",
"filter-default": "預設圖示",
"icon_tooltip": "{{name}}\n圖示包{{iconPack}}",
"no_results": "找不到圖示。"
},
"basic_properties": {
"note_type": "筆記類型",
@@ -789,7 +797,8 @@
"expand_tooltip": "展開此集合的直接子級(單層深度)。按下右側箭頭以查看更多選項。",
"expand_first_level": "展開直接子級",
"expand_nth_level": "展開 {{depth}} 層",
"expand_all_levels": "展開所有層級"
"expand_all_levels": "展開所有層級",
"hide_child_notes": "隱藏樹中的子筆記"
},
"edited_notes": {
"no_edited_notes_found": "今天還沒有編輯過的筆記...",
@@ -1404,7 +1413,7 @@
"will_be_deleted_in": "此附件將在 {{time}} 後自動刪除",
"will_be_deleted_soon": "該附件即將被自動刪除",
"deletion_reason": ",因為該附件未連結在筆記的內容中。為防止被刪除,請將附件連結重新新增至內容中或將附件轉換為筆記。",
"role_and_size": "角色:{{role}},大小:{{size}}",
"role_and_size": "角色:{{role}},大小:{{size}}MIME{{- mimeType}}",
"link_copied": "已複製附件連結到剪貼簿。",
"unrecognized_role": "無法識別的附件角色 '{{role}}'。"
},
@@ -1458,7 +1467,10 @@
"duplicate": "複製副本",
"open-in-popup": "快速編輯",
"archive": "封存",
"unarchive": "解除封存"
"unarchive": "解除封存",
"open-in-a-new-window": "在新視窗打開",
"hide-subtree": "隱藏子階層",
"show-subtree": "顯示子階層"
},
"shared_info": {
"help_link": "如需幫助,請訪問 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>。",
@@ -1548,7 +1560,15 @@
"create-child-note": "建立子筆記",
"unhoist": "取消聚焦",
"toggle-sidebar": "切換側邊欄",
"dropping-not-allowed": "不允許移動筆記至此處。"
"dropping-not-allowed": "不允許移動筆記至此處。",
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}}",
"shared-indicator-tooltip": "此筆記已公開分享",
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}",
"subtree-hidden-tooltip_one": "從樹中隱藏的 {{count}} 篇子筆記",
"subtree-hidden-moved-title": "已新增至 {{title}}",
"subtree-hidden-moved-description-collection": "此集合隱藏其樹中的子筆記。",
"subtree-hidden-moved-description-other": "子筆記隱藏於此筆記的樹中。"
},
"title_bar_buttons": {
"window-on-top": "保持此視窗置頂"
@@ -1556,7 +1576,12 @@
"note_detail": {
"could_not_find_typewidget": "找不到類型為 '{{type}}' 的 typeWidget",
"printing": "正在列印…",
"printing_pdf": "正在匯出為 PDF…"
"printing_pdf": "正在匯出為 PDF…",
"print_report_title": "列印報告",
"print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。",
"print_report_collection_content_other": "",
"print_report_collection_details_button": "查看詳情",
"print_report_collection_details_ignored_notes": "忽略的筆記"
},
"note_title": {
"placeholder": "請輸入筆記標題...",
@@ -1566,7 +1591,8 @@
"note_type_switcher_others": "其他筆記類型",
"note_type_switcher_templates": "模板",
"note_type_switcher_collection": "集合",
"edited_notes": "編輯過的筆記"
"edited_notes": "今天編輯過的筆記",
"promoted_attributes": "升級屬性"
},
"search_result": {
"no_notes_found": "沒有找到符合搜尋條件的筆記。",
@@ -2182,7 +2208,14 @@
"read_only_temporarily_disabled_description": "此筆記目前可編輯,但通常為唯讀狀態。當您切換至其他筆記時,本筆記將立即恢復為唯讀模式。\n\n點擊此處重新啟用唯讀模式。",
"clipped_note_description": "本筆記原始來源為 {{url}}。\n\n點擊此處前往原網頁。",
"execute_script_description": "此筆記為腳本筆記。點擊以執行腳本。",
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。"
"execute_sql_description": "此筆記為 SQL 筆記。點擊以執行 SQL 查詢。",
"save_status_saved": "已儲存",
"save_status_saving": "正在儲存…",
"save_status_unsaved": "未儲存",
"save_status_error": "儲存失敗",
"save_status_saving_tooltip": "正在儲存更動。",
"save_status_unsaved_tooltip": "仍有更動尚未儲存。它們將在稍後自動儲存。",
"save_status_error_tooltip": "在儲存筆記時發生錯誤。如果可以,請嘗試將筆記內容複製至他處並重新載入應用程式。"
},
"breadcrumb": {
"hoisted_badge": "聚焦",
@@ -2219,5 +2252,15 @@
},
"attributes_panel": {
"title": "筆記屬性"
},
"pdf": {
"attachments_one": "{{count}} 個附件",
"attachments_other": "",
"layers_one": "{{count}} 層",
"layers_other": "",
"pages_one": "共 {{count}} 頁",
"pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "正在載入…"
}
}

View File

@@ -321,7 +321,6 @@
"download_button": "Завантажити",
"mime": "МІМЕ: ",
"file_size": "Розмір файлу:",
"preview": "Попередній перегляд:",
"preview_not_available": "Попередній перегляд недоступний для цього типу нотатки.",
"diff_on": "Показати різницю",
"diff_off": "Показати вміст",

View File

@@ -69,7 +69,7 @@ declare namespace Fancytree {
debug(msg: any): void;
/** Expand (or collapse) all parent nodes. */
expandAll(flag?: boolean, options?: Object): void;
expandAll(flag?: boolean, options?: object): void;
/** [ext-filter] Dimm or hide whole branches.
* @returns {integer} count
@@ -221,6 +221,7 @@ declare namespace Fancytree {
branchId: string;
isProtected: boolean;
noteType: NoteType;
subtreeHidden: boolean;
}
interface FancytreeNewNode extends FancytreeNodeData {
@@ -369,7 +370,7 @@ declare namespace Fancytree {
* @param mode 'before', 'after', or 'child' (default='child')
* @param init NodeData (or simple title string)
*/
editCreateNode(mode?: string, init?: Object): void;
editCreateNode(mode?: string, init?: object): void;
/** [ext-edit] Stop inline editing.
*
@@ -526,7 +527,7 @@ declare namespace Fancytree {
*
* @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
*/
makeVisible(opts?: Object): JQueryPromise<any>;
makeVisible(opts?: object): JQueryPromise<any>;
/** Move this node to targetNode.
*
@@ -589,25 +590,25 @@ declare namespace Fancytree {
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: boolean, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: boolean, options?: object): JQueryPromise<any>;
/**
* @param effects animation options.
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
*/
scrollIntoView(effects?: Object, options?: Object): JQueryPromise<any>;
scrollIntoView(effects?: object, options?: object): JQueryPromise<any>;
/**
* @param flag pass false to deactivate
* @param opts additional options. Defaults to {noEvents: false}
*/
setActive(flag?: boolean, opts?: Object): JQueryPromise<any>;
setActive(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* @param flag pass false to collapse.
* @param opts additional options. Defaults to {noAnimation:false, noEvents:false}
*/
setExpanded(flag?: boolean, opts?: Object): JQueryPromise<any>;
setExpanded(flag?: boolean, opts?: object): JQueryPromise<any>;
/**
* Set keyboard focus to this node.
@@ -1109,7 +1110,7 @@ declare namespace Fancytree {
/** class names added to the node markup (separate with space) */
extraClasses?: string | undefined;
/** all properties from will be copied to `node.data` */
data?: Object | undefined;
data?: object | undefined;
/** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */
iconTooltip?: string | undefined;
@@ -1160,7 +1161,7 @@ declare namespace Fancytree {
escapeHtml(s: string): string;
getEventTarget(event: Event): Object;
getEventTarget(event: Event): object;
getEventTargetType(event: Event): string;
@@ -1179,7 +1180,7 @@ declare namespace Fancytree {
parseHtml($ul: JQuery): NodeData[];
/** Add Fancytree extension definition to the list of globally available extensions. */
registerExtension(definition: Object): void;
registerExtension(definition: object): void;
unescapeHtml(s: string): string;

121
apps/client/src/types-pdfjs.d.ts vendored Normal file
View File

@@ -0,0 +1,121 @@
type HistoryData = {
files: {
fingerprint: string;
page: number;
zoom: string;
scrollLeft: number;
scrollTop: number;
rotation: number;
sidebarView: number;
}[];
};
interface Window {
/**
* By default, pdf.js will try to store information about the opened PDFs such as zoom and scroll position in local storage.
* The Trilium alternative is to use attachments stored at note level.
* This variable represents the direct content used by the pdf.js viewer in its local storage key, but in plain JS object format.
* The variable must be set early at startup, before pdf.js fully initializes.
*/
TRILIUM_VIEW_HISTORY_STORE?: HistoryData;
/**
* If set to true, hides the pdf.js viewer default sidebar containing the outline, page navigation, etc.
* This needs to be set early in the main method.
*/
TRILIUM_HIDE_SIDEBAR?: boolean;
TRILIUM_NOTE_ID: string;
TRILIUM_NTX_ID: string | null | undefined;
}
interface PdfOutlineItem {
title: string;
level: number;
dest: unknown;
id: string;
items: PdfOutlineItem[];
}
interface WithContext {
ntxId: string;
noteId: string | null | undefined;
}
interface PdfDocumentModifiedMessage extends WithContext {
type: "pdfjs-viewer-document-modified";
}
interface PdfDocumentBlobResultMessage extends WithContext {
type: "pdfjs-viewer-blob";
data: Uint8Array<ArrayBufferLike>;
}
interface PdfSaveViewHistoryMessage extends WithContext {
type: "pdfjs-viewer-save-view-history";
data: string;
}
interface PdfViewerTocMessage {
type: "pdfjs-viewer-toc";
data: PdfOutlineItem[];
}
interface PdfViewerActiveHeadingMessage {
type: "pdfjs-viewer-active-heading";
headingId: string;
}
interface PdfViewerPageInfoMessage {
type: "pdfjs-viewer-page-info";
totalPages: number;
currentPage: number;
}
interface PdfViewerCurrentPageMessage {
type: "pdfjs-viewer-current-page";
currentPage: number;
}
interface PdfViewerThumbnailMessage {
type: "pdfjs-viewer-thumbnail";
pageNumber: number;
dataUrl: string;
}
interface PdfAttachment {
filename: string;
size: number;
}
interface PdfViewerAttachmentsMessage {
type: "pdfjs-viewer-attachments";
attachments: PdfAttachment[];
downloadAttachment?: (fileName: string) => void;
}
interface PdfLayer {
id: string;
name: string;
visible: boolean;
}
interface PdfViewerLayersMessage {
type: "pdfjs-viewer-layers";
layers: PdfLayer[];
toggleLayer?: (layerId: string, visible: boolean) => void;
}
type PdfMessageEvent = MessageEvent<
PdfDocumentModifiedMessage
| PdfSaveViewHistoryMessage
| PdfViewerTocMessage
| PdfViewerActiveHeadingMessage
| PdfViewerPageInfoMessage
| PdfViewerCurrentPageMessage
| PdfViewerThumbnailMessage
| PdfViewerAttachmentsMessage
| PdfViewerLayersMessage
| PdfDocumentBlobResultMessage
>;

View File

@@ -1,4 +1,4 @@
import { IconRegistry } from "@triliumnext/commons";
import { IconRegistry, Locale } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -47,14 +47,25 @@ interface CustomGlobals {
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;
declare global {
interface Window {
$: JQueryStatic;
jQuery: JQueryStatic;
logError(message: string);
logInfo(message: string);

View File

@@ -215,7 +215,7 @@ export default function NoteDetail() {
return (
<div
ref={containerRef}
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
>
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper

View File

@@ -1,11 +1,17 @@
import clsx from "clsx";
import { t } from "../../services/i18n";
import options from "../../services/options";
import ActionButton from "../react/ActionButton";
import { useTriliumOptionBool } from "../react/hooks";
import { useState, useCallback } from "preact/hooks";
import { useTriliumEvent } from "../react/hooks";
export default function RightPaneToggle() {
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
useTriliumEvent("toggleRightPane", useCallback(() => {
setRightPaneVisible(current => !current);
}, []));
return (
<ActionButton
@@ -15,7 +21,7 @@ export default function RightPaneToggle() {
)}
text={t("right_pane.toggle")}
icon="bx bx-sidebar"
onClick={() => setRightPaneVisible(!rightPaneVisible)}
triggerCommand="toggleRightPane"
/>
);
}

View File

@@ -11,7 +11,8 @@ import froca from "../../services/froca";
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { allViewTypes, ViewModeMedia, ViewModeProps, ViewTypeOptions } from "./interface";
import ViewModeStorage from "./view_mode_storage";
import ViewModeStorage, { type ViewModeStorageType } from "./view_mode_storage";
interface NoteListProps {
note: FNote | null | undefined;
notePath: string | null | undefined;
@@ -215,7 +216,7 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
return noteIds;
}
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewTypeOptions | undefined) {
export function useViewModeConfig<T extends object>(note: FNote | null | undefined, viewType: ViewModeStorageType | undefined) {
const [ viewConfig, setViewConfig ] = useState<{
config: T | undefined;
storeFn: (data: T) => void;

View File

@@ -1,27 +1,29 @@
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { ViewModeProps } from "../interface";
import Calendar from "./calendar";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./index.css";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { Calendar as FullCalendar } from "@fullcalendar/core";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { changeEvent, newEvent } from "./api";
import froca from "../../../services/froca";
import date_notes from "../../../services/date_notes";
import appContext from "../../../components/app_context";
import { DateSelectArg, EventChangeArg, EventMountArg, EventSourceFuncArg, LocaleInput, PluginDef } from "@fullcalendar/core/index.js";
import { DateClickArg } from "@fullcalendar/interaction";
import FNote from "../../../entities/fnote";
import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { RefObject } from "preact";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { openCalendarContextMenu } from "./context_menu";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
import date_notes from "../../../services/date_notes";
import dialog from "../../../services/dialog";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import { isMobile } from "../../../services/utils";
import ActionButton from "../../react/ActionButton";
import Button, { ButtonGroup } from "../../react/Button";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { changeEvent, newEvent } from "./api";
import Calendar from "./calendar";
import { openCalendarContextMenu } from "./context_menu";
import { buildEvents, buildEventsForCalendar } from "./event_builder";
import { parseStartEndDateFromEvent, parseStartEndTimeFromEvent } from "./utils";
interface CalendarViewData {
@@ -59,7 +61,7 @@ const CALENDAR_VIEWS = [
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
}
]
];
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
@@ -75,6 +77,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
ru: () => import("@fullcalendar/core/locales/ru"),
ja: () => import("@fullcalendar/core/locales/ja"),
pt: () => import("@fullcalendar/core/locales/pt"),
pl: () => import("@fullcalendar/core/locales/pl"),
"pt_br": () => import("@fullcalendar/core/locales/pt-br"),
uk: () => import("@fullcalendar/core/locales/uk"),
en: null,
@@ -102,9 +105,9 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
const eventBuilder = useMemo(() => {
if (!isCalendarRoot) {
return async () => await buildEvents(noteIds);
} else {
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}
}
return async (e: EventSourceFuncArg) => await buildEventsForCalendar(note, e);
}, [isCalendarRoot, noteIds]);
const plugins = usePlugins(isEditable, isCalendarRoot);
@@ -178,7 +181,7 @@ function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar>
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup>
</div>
)
);
}
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
@@ -293,7 +296,7 @@ function useEventDisplayCustomization(parentNote: FNote) {
if (promotedAttributes) {
let promotedAttributesHtml = "";
for (const [name, value] of promotedAttributes) {
promotedAttributesHtml = promotedAttributesHtml + /*html*/`\
promotedAttributesHtml = `${promotedAttributesHtml /*html*/}\
<div class="promoted-attribute">
<span class="promoted-attribute-name">${name}</span>: <span class="promoted-attribute-value">${value}</span>
</div>`;

View File

@@ -142,4 +142,10 @@
border: 1px solid var(--main-border-color);
background: var(--more-accented-background-color);
}
.note-list.grid-view .note-path {
margin-left: 0.5em;
vertical-align: middle;
opacity: 0.5;
}
/* #endregion */

View File

@@ -7,7 +7,6 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n";
import link from "../../../services/link";
import tree from "../../../services/tree";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
@@ -45,6 +44,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
return (
<div class="note-list grid-view">
@@ -53,7 +53,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} includeArchived={includeArchived} />
))}
</div>
@@ -95,24 +95,17 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
</h5>
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
</>}
</div>
);
}
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) {
const titleRef = useRef<HTMLSpanElement>(null);
const [ noteTitle, setNoteTitle ] = useState<string>();
const notePath = getNotePath(parentNote, note);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
tree.getNoteTitle(note.noteId, parentNote.noteId).then(setNoteTitle);
}, [ note ]);
useEffect(() => highlightSearch(titleRef.current), [ noteTitle, highlightedTokens ]);
return (
<div
@@ -123,13 +116,14 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
>
<h5 className="note-book-header">
<Icon className="note-icon" icon={note.getIcon()} />
<span ref={titleRef} className="note-book-title">{noteTitle}</span>
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} />
</h5>
<NoteContent
note={note}
trim
highlightedTokens={highlightedTokens}
includeArchivedNotes={includeArchived}
/>
</div>
);
@@ -146,14 +140,22 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) {
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;
highlightedTokens: string[] | null | undefined;
includeArchivedNotes: boolean;
}) {
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
content_renderer.getRenderedContent(note, {
trim,
noChildrenList
noChildrenList,
noIncludedNotes: true,
includeArchivedNotes
})
.then(({ $renderedContent, type }) => {
if (!contentRef.current) return;

View File

@@ -4,14 +4,16 @@ import { ViewTypeOptions } from "../collections/interface";
const ATTACHMENT_ROLE = "viewConfig";
export type ViewModeStorageType = ViewTypeOptions | "pdfHistory";
export default class ViewModeStorage<T extends object> {
private note: FNote;
private attachmentName: string;
constructor(note: FNote, viewType: ViewTypeOptions) {
constructor(note: FNote, viewType: ViewModeStorageType) {
this.note = note;
this.attachmentName = viewType + ".json";
this.attachmentName = `${viewType}.json`;
}
async store(data: T) {

View File

@@ -1,10 +1,11 @@
import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import Component from "../../components/component.js";
import NoteContext from "../../components/note_context.js";
import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import NoteContext from "../../components/note_context.js";
import type BasicWidget from "../basic_widget.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import FlexContainer from "./flex_container.js";
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
@@ -74,7 +75,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
const subContexts = activeContext.getSubContexts();
let noteContext: NoteContext | undefined = undefined;
let noteContext: NoteContext | undefined;
if (isMobile() && subContexts.length > 1) {
noteContext = subContexts.find(s => s.ntxId !== ntxId);
}
@@ -201,6 +202,11 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
async refresh() {
this.toggleExt(true);
// Mark the active note context.
for (const child of this.children as NoteContextAwareWidget[]) {
child.$widget.toggleClass("active", !!child.noteContext?.isActive());
}
}
toggleInt(show: boolean) {} // not needed
@@ -239,16 +245,16 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
widget.hasBeenAlreadyShown = true;
return [widget.handleEvent("noteSwitched", noteSwitchedContext), this.refreshNotShown(noteSwitchedContext)];
} else {
return Promise.resolve();
}
return Promise.resolve();
}
if (name === "activeContextChanged") {
return this.refreshNotShown(data as EventData<"activeContextChanged">);
} else {
return super.handleEventInChildren(name, data);
}
return super.handleEventInChildren(name, data);
}
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {

View File

@@ -88,6 +88,7 @@ export default function PopupEditor() {
onHidden={() => setShown(false)}
keepInDom // needed for faster loading
noFocus // automatic focus breaks block popup
stackable
>
{!isNewLayout && <ReadOnlyNoteInfoBar />}
<PromotedAttributes />

View File

@@ -14,7 +14,7 @@ body.mobile .revisions-dialog {
flex-grow: 1;
width: 100%;
}
.modal-body {
height: fit-content !important;
flex-direction: column;
@@ -24,7 +24,7 @@ body.mobile .revisions-dialog {
.modal-footer {
font-size: 0.9em;
}
.revision-list {
height: fit-content !important;
max-height: 20vh;
@@ -32,7 +32,7 @@ body.mobile .revisions-dialog {
padding: 0 1em;
flex-shrink: 0;
}
.modal-body > .revision-content-wrapper {
flex-grow: 1;
max-width: unset !important;
@@ -40,24 +40,68 @@ body.mobile .revisions-dialog {
margin: 0;
display: block !important;
}
.modal-body > .revision-content-wrapper > div:first-of-type {
flex-direction: column;
}
.revision-title {
font-size: 1rem;
}
.revision-title-buttons {
text-align: center;
display: flex;
gap: 0.25em;
flex-wrap: wrap;
}
.revision-content {
padding: 0.5em;
height: fit-content;
}
}
}
.revisions-dialog {
.revision-title-buttons {
flex-shrink: 0;
}
.revision-list {
flex-shrink: 0;
}
.revision-content.type-file {
display: flex;
min-width: 0;
min-height: 0;
flex-grow: 1;
.file-preview-table {
th,
td {
padding: 0.25em 0;
}
}
.revision-file-preview {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
flex-grow: 1;
}
.revision-file-preview-content {
flex-grow: 1;
min-height: 0;
display: flex;
flex-direction: column;
> * {
height: 100%;
}
}
}
}

View File

@@ -1,26 +1,31 @@
import type { RevisionPojo, RevisionItem } from "@triliumnext/commons";
import "./revisions.css";
import type { RevisionItem, RevisionPojo } from "@triliumnext/commons";
import clsx from "clsx";
import { diffWords } from "diff";
import type { CSSProperties } from "preact/compat";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import dialog from "../../services/dialog";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { renderMathInElement } from "../../services/math";
import open from "../../services/open";
import options from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import toast from "../../services/toast";
import Button from "../react/Button";
import FormToggle from "../react/FormToggle";
import Modal from "../react/Modal";
import FormList, { FormListItem } from "../react/FormList";
import utils from "../../services/utils";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import protected_session_holder from "../../services/protected_session_holder";
import { renderMathInElement } from "../../services/math";
import type { CSSProperties } from "preact/compat";
import open from "../../services/open";
import ActionButton from "../react/ActionButton";
import options from "../../services/options";
import Button from "../react/Button";
import FormList, { FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useTriliumEvent } from "../react/hooks";
import { diffWords } from "diff";
import "./revisions.css";
import Modal from "../react/Modal";
import { RawHtmlBlock } from "../react/RawHtml";
import PdfViewer from "../type_widgets/file/PdfViewer";
export default function RevisionsDialog() {
const [ note, setNote ] = useState<FNote>();
@@ -47,7 +52,7 @@ export default function RevisionsDialog() {
setRevisions(undefined);
setNoteContent(undefined);
}
}, [ note?.noteId, refreshCounter ]);
}, [ note, refreshCounter ]);
if (revisions?.length && !currentRevision) {
setCurrentRevision(revisions[0]);
@@ -102,38 +107,38 @@ export default function RevisionsDialog() {
setRevisions(undefined);
}}
show={shown}
>
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
}}
currentRevision={currentRevision}
/>
>
<RevisionsList
revisions={revisions ?? []}
onSelect={(revisionId) => {
const correspondingRevision = (revisions ?? []).find((r) => r.revisionId === revisionId);
if (correspondingRevision) {
setCurrentRevision(correspondingRevision);
}
}}
currentRevision={currentRevision}
/>
<div className="revision-content-wrapper" style={{
flexGrow: "1",
marginInlineStart: "20px",
display: "flex",
flexDirection: "column",
maxWidth: "calc(100% - 150px)",
minWidth: 0
}}>
<RevisionPreview
noteContent={noteContent}
revisionItem={currentRevision}
showDiff={showDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}} />
</div>
<div className="revision-content-wrapper" style={{
flexGrow: "1",
marginInlineStart: "20px",
display: "flex",
flexDirection: "column",
maxWidth: "calc(100% - 150px)",
minWidth: 0
}}>
<RevisionPreview
noteContent={noteContent}
revisionItem={currentRevision}
showDiff={showDiff}
setShown={setShown}
onRevisionDeleted={() => {
setRefreshCounter(c => c + 1);
setCurrentRevision(undefined);
}} />
</div>
</Modal>
)
);
}
function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: RevisionItem[], onSelect: (val: string) => void, currentRevision?: RevisionItem }) {
@@ -141,6 +146,7 @@ function RevisionsList({ revisions, onSelect, currentRevision }: { revisions: Re
<FormList onSelect={onSelect} fullHeight wrapperClassName="revision-list">
{revisions.map((item) =>
<FormListItem
key={item.revisionId}
value={item.revisionId}
active={currentRevision && item.revisionId === currentRevision.revisionId}
>
@@ -202,14 +208,17 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
text={t("revisions.download_button")}
onClick={() => {
if (revisionItem.revisionId) {
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId)}
}
open.downloadRevision(revisionItem.noteId, revisionItem.revisionId);}
}
}/>
</>
}
</div>)}
</div>
<div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
<div
className={clsx("revision-content use-tn-links selectable-text", `type-${revisionItem?.type}`)}
style={{ overflow: "auto", wordBreak: "break-word" }}
>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div>
</>
@@ -230,16 +239,16 @@ const CODE_STYLE: CSSProperties = {
function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: { noteContent?:string, revisionItem?: RevisionItem, fullRevision?: RevisionPojo, showDiff: boolean}) {
const content = fullRevision?.content;
if (!revisionItem || !content) {
if (!revisionItem || !fullRevision) {
return <></>;
}
if (showDiff) {
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>
return <RevisionContentDiff noteContent={noteContent} itemContent={content} itemType={revisionItem.type}/>;
}
switch (revisionItem.type) {
case "text":
return <RevisionContentText content={content} />
return <RevisionContentText content={content} />;
case "code":
return <pre style={CODE_STYLE}>{content}</pre>;
case "image":
@@ -256,28 +265,11 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
// as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be uploaded as a new note
return <img
src={`data:${fullRevision.mime};base64,${fullRevision.content}`}
style={IMAGE_STYLE} />
style={IMAGE_STYLE} />;
}
}
case "file":
return <table cellPadding="10">
<tr>
<th>{t("revisions.mime")}</th>
<td>{revisionItem.mime}</td>
</tr>
<tr>
<th>{t("revisions.file_size")}</th>
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
</tr>
{fullRevision.content &&
<tr>
<td colspan={2}>
<strong>{t("revisions.preview")}</strong>
<pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>
</td>
</tr>
}
</table>;
return <FilePreview fullRevision={fullRevision} revisionItem={revisionItem} />;
case "canvas":
case "mindMap":
case "mermaid": {
@@ -287,7 +279,7 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }:
style={IMAGE_STYLE} />;
}
default:
return <>{t("revisions.preview_not_available")}</>
return <>{t("revisions.preview_not_available")}</>;
}
}
@@ -298,7 +290,7 @@ function RevisionContentText({ content }: { content: string | Buffer<ArrayBuffer
renderMathInElement(contentRef.current, { trust: true });
}
}, [content]);
return <div ref={contentRef} className="ck-content" dangerouslySetInnerHTML={{ __html: content as string }}></div>
return <RawHtmlBlock containerRef={contentRef} className="ck-content" html={content as string} />;
}
function RevisionContentDiff({ noteContent, itemContent, itemType }: {
@@ -330,9 +322,9 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
return `<span class="revision-diff-added">${utils.escapeHtml(part.value)}</span>`;
} else if (part.removed) {
return `<span class="revision-diff-removed">${utils.escapeHtml(part.value)}</span>`;
} else {
return utils.escapeHtml(part.value);
}
return utils.escapeHtml(part.value);
}).join("");
if (contentRef.current) {
@@ -340,7 +332,7 @@ function RevisionContentDiff({ noteContent, itemContent, itemType }: {
}
}, [noteContent, itemContent, itemType]);
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }}></div>;
return <div ref={contentRef} className="ck-content" style={{ whiteSpace: "pre-wrap" }} />;
}
function RevisionFooter({ note }: { note?: FNote }) {
@@ -348,7 +340,7 @@ function RevisionFooter({ note }: { note?: FNote }) {
return <></>;
}
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "");
let revisionsNumberLimit: number | string = parseInt(note?.getLabelValue("versioningLimit") ?? "", 10);
if (!Number.isInteger(revisionsNumberLimit)) {
revisionsNumberLimit = options.getInt("revisionSnapshotNumberLimit") ?? 0;
}
@@ -370,10 +362,67 @@ function RevisionFooter({ note }: { note?: FNote }) {
</>;
}
function FilePreview({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
return (
<div className="revision-file-preview">
<table className="file-preview-table">
<tbody>
<tr>
<th>{t("revisions.mime")}</th>
<td>{revisionItem.mime}</td>
</tr>
<tr>
<th>{t("revisions.file_size")}</th>
<td>{revisionItem.contentLength && utils.formatSize(revisionItem.contentLength)}</td>
</tr>
</tbody>
</table>
<div class="revision-file-preview-content">
<FilePreviewInner revisionItem={revisionItem} fullRevision={fullRevision} />
</div>
</div>
);
}
function FilePreviewInner({ revisionItem, fullRevision }: { revisionItem: RevisionItem, fullRevision: RevisionPojo }) {
if (revisionItem.mime.startsWith("audio/")) {
return (
<audio
src={`api/revisions/${revisionItem.revisionId}/download`}
controls
/>
);
}
if (revisionItem.mime.startsWith("video/")) {
return (
<video
src={`api/revisions/${revisionItem.revisionId}/download`}
controls
/>
);
}
if (revisionItem.mime === "application/pdf") {
return (
<PdfViewer
pdfUrl={`../../api/revisions/${revisionItem.revisionId}/download`}
/>
);
}
if (fullRevision.content) {
return <pre className="file-preview-content" style={CODE_STYLE}>{fullRevision.content}</pre>;
}
return t("revisions.preview_not_available");
}
async function getNote(noteId?: string | null) {
if (noteId) {
return await froca.getNote(noteId);
} else {
return appContext.tabManager.getActiveContextNote();
}
return appContext.tabManager.getActiveContextNote();
}

View File

@@ -1,17 +1,18 @@
import { useCallback, useLayoutEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { isDesktop, isMobile } from "../../services/utils";
import CalendarWidget from "./CalendarWidget";
import SpacerWidget from "./SpacerWidget";
import BookmarkButtons from "./BookmarkButtons";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SyncStatus from "./SyncStatus";
import HistoryNavigationButton from "./HistoryNavigation";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import { useTriliumEvent } from "../react/hooks";
import { onWheelHorizontalScroll } from "../widget_utils";
import BookmarkButtons from "./BookmarkButtons";
import CalendarWidget from "./CalendarWidget";
import HistoryNavigationButton from "./HistoryNavigation";
import { LaunchBarContext } from "./launch_bar_widgets";
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SpacerWidget from "./SpacerWidget";
import SyncStatus from "./SyncStatus";
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const childNotes = useLauncherChildNotes();
@@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal
}}>
{childNotes?.map(childNote => {
if (childNote.type !== "launcher") {
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
return false;
}
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
return false;
}
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />;
})}
</LaunchBarContext.Provider>
</div>
)
);
}
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
@@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return <CalendarWidget launcherNote={note} />
return <CalendarWidget launcherNote={note} />;
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
@@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
case "syncStatus":
return <SyncStatus />;
case "backInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
case "forwardInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />;
case "todayInJournal":
return <TodayLauncher launcherNote={note} />
return <TodayLauncher launcherNote={note} />;
case "quickSearch":
return <QuickSearchLauncherWidget />
return <QuickSearchLauncherWidget />;
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />
return <AiChatButton launcherNote={note} />;
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}

View File

@@ -2,6 +2,11 @@
contain: none;
}
@keyframes fadeOut {
from { opacity: var(--default-opacity); }
to { opacity: 0; }
}
.note-badges {
display: flex;
gap: 5px;
@@ -16,6 +21,23 @@
&.share-badge {--color: var(--badge-share-background-color);}
&.clipped-note-badge {--color: var(--badge-clipped-note-background-color);}
&.execute-badge {--color: var(--badge-execute-background-color);}
&.save-status-badge {
--default-opacity: 0.4;
opacity: var(--default-opacity);
transition: opacity 250ms ease-in;
color: var(--main-text-color);
&.error {
color: var(--dropdown-item-icon-destructive-color);
opacity: 1;
}
&.saved {
animation: fadeOut 250ms ease-in 5s forwards;
pointer-events: none;
}
}
min-width: 0;
.text {

View File

@@ -1,17 +1,20 @@
import "./NoteBadges.css";
import { clsx } from "clsx";
import { copyTextWithToast } from "../../services/clipboard_ext";
import { t } from "../../services/i18n";
import { goToLinkExt } from "../../services/link";
import { Badge, BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
export default function NoteBadges() {
return (
<div className="note-badges">
<SaveStatusBadge />
<ReadOnlyBadge />
<ShareBadge />
<ClippedNoteBadge />
@@ -105,3 +108,42 @@ function ExecuteBadge() {
/>
);
}
export function SaveStatusBadge() {
const saveState = useGetContextData("saveState");
if (!saveState) return;
const stateConfig = {
saved: {
icon: "bx bx-check",
title: t("breadcrumb_badges.save_status_saved"),
tooltip: undefined
},
saving: {
icon: "bx bx-loader bx-spin",
title: t("breadcrumb_badges.save_status_saving"),
tooltip: t("breadcrumb_badges.save_status_saving_tooltip")
},
unsaved: {
icon: "bx bx-pencil",
title: t("breadcrumb_badges.save_status_unsaved"),
tooltip: t("breadcrumb_badges.save_status_unsaved_tooltip")
},
error: {
icon: "bx bxs-error",
title: t("breadcrumb_badges.save_status_error"),
tooltip: t("breadcrumb_badges.save_status_error_tooltip")
}
};
const { icon, title, tooltip } = stateConfig[saveState.state];
return (
<Badge
className={clsx("save-status-badge", saveState.state)}
icon={icon}
text={title}
tooltip={tooltip}
/>
);
}

View File

@@ -5,7 +5,7 @@ import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren, RefObject } from "preact";
import { createPortal } from "preact/compat";
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
@@ -338,15 +338,19 @@ interface AttributesProps extends StatusBarContext {
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
const [ count, setCount ] = useState(note.attributes.length);
const getAttributeCount = useCallback((note: FNote) => {
return note.getAttributes().filter(a => !a.isAutoLink).length;
}, []);
// React to note changes.
useEffect(() => {
setCount(note.getAttributes().filter(a => !a.isAutoLink).length);
}, [ note ]);
setCount(getAttributeCount(note));
}, [ note, getAttributeCount ]);
// React to changes in count.
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
setCount(note.attributes.length);
setCount(getAttributeCount(note));
}
}));

View File

@@ -82,6 +82,13 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
))}
{properties.length > 0 && <FormDropdownDivider />}
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-hide",
label: t("book_properties.hide_child_notes"),
bindToLabel: "subtreeHidden"
} as CheckBoxProperty} />
<ViewProperty note={note} property={{
type: "checkbox",
icon: "bx bx-archive",

View File

@@ -0,0 +1,20 @@
#left-pane .tree-wrapper {
.note-indicator-icon.subtree-hidden-badge {
font-family: inherit !important;
margin-inline: 0.5em;
margin-top: 0.3em;
background: var(--left-pane-item-action-button-background);
color: var(--left-pane-item-action-button-color);
padding: 0.1em 0.6em;
border-radius: 0.5em;
font-size: 0.7rem;
font-weight: normal;
float: right;
vertical-align: middle;
}
.spotlighted-node {
opacity: 0.8;
font-style: italic;
}
}

View File

@@ -3,13 +3,13 @@ import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
import "../stylesheets/tree.css";
import "./note_tree.css";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js";
import type FBranch from "../entities/fbranch.js";
import type FNote from "../entities/fnote.js";
import type { NoteType } from "../entities/fnote.js";
import contextMenu from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import branchService from "../services/branches.js";
@@ -153,7 +153,7 @@ const TPL = /*html*/`
const MAX_SEARCH_RESULTS_IN_TREE = 100;
// this has to be hanged on the actual elements to effectively intercept and stop click event
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation();
// TODO: Fix once we remove Node.js API from public
type Timeout = NodeJS.Timeout | string | number | undefined;
@@ -190,6 +190,9 @@ export interface DragData {
export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node";
/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */
const BATCH_UPDATE_THRESHOLD = 10;
export default class NoteTreeWidget extends NoteContextAwareWidget {
private $tree!: JQuery<HTMLElement>;
private $treeActions!: JQuery<HTMLElement>;
@@ -201,6 +204,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
private treeName: "main";
private autoCollapseTimeoutId?: Timeout;
private lastFilteredHoistedNotePath?: string | null;
private spotlightedNotePath?: string | null;
private spotlightedNode: Fancytree.FancytreeNode | null = null;
private tree!: Fancytree.Fancytree;
constructor() {
@@ -353,6 +358,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
this.$tree.fancytree({
titlesTabbable: true,
keyboard: true,
toggleEffect: options.is("motionEnabled") ? undefined : false,
extensions: ["dnd5", "clones", "filter"],
source: treeData,
scrollOfs: {
@@ -552,7 +558,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (data.hitMode === "after") {
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
} else if (data.hitMode === "over") {
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
} else {
throw new Error(`Unknown hitMode '${data.hitMode}'`);
}
@@ -598,73 +604,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
clones: {
highlightActiveClones: true
},
async enhanceTitle (
event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}
) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = await froca.getNote(node.data.noteId, true);
if (!note) {
return;
}
const activeNoteContext = appContext.tabManager.getActiveContext();
const $span = $(node.span);
$span.find(".tree-item-button").remove();
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
) {
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($createChildNoteButton);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
},
enhanceTitle: buildEnhanceTitle(),
// this is done to automatically lazy load all expanded notes after tree load
loadChildren: (event, data) => {
data.node.visit((subNode) => {
@@ -774,6 +714,23 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
}
if (parentNote.isLabelTruthy("subtreeHidden")) {
// If we have a spotlighted note path, show only the child that leads to it
if (this.spotlightedNotePath) {
const spotlightPathSegments = this.spotlightedNotePath.split('/');
const parentIndex = spotlightPathSegments.indexOf(parentNote.noteId);
if (parentIndex >= 0 && parentIndex < spotlightPathSegments.length - 1) {
const nextNoteIdInPath = spotlightPathSegments[parentIndex + 1];
childBranches = childBranches.filter(branch => branch.noteId === nextNoteIdInPath);
} else {
childBranches = [];
}
} else {
childBranches = [];
}
}
for (const branch of childBranches) {
if (hideArchivedNotes) {
const note = branch.getNoteFromCache();
@@ -845,6 +802,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
lazy: true,
folder: isFolder,
expanded: !!branch.isExpanded && note.type !== "search",
subtreeHidden: note.isLabelTruthy("subtreeHidden"),
key: utils.randomString(12) // this should prevent some "duplicate key" errors
};
@@ -903,6 +861,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
extraClasses.push(...["tinted", colorClass]);
}
if (this.spotlightedNotePath && this.spotlightedNotePath.endsWith(`/${note.noteId}`)) {
extraClasses.push("spotlighted-node");
}
return extraClasses.join(" ");
}
@@ -1053,18 +1015,43 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
foundChildNode = this.findChildNode(parentNode, childNoteId);
if (!foundChildNode) {
if (logErrors) {
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
const childNote = await froca.getNote(childNoteId);
const childNote = await froca.getNote(childNoteId);
if (childNote?.type === "image") return;
if (!childNote || childNote.type !== "image") {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
// The child note can be part of a note with #subtreeHidden, case in which we need to "spotlight" it.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId);
if (parentNote?.isLabelTruthy("subtreeHidden")) {
// Enable spotlight mode and reload the parent to show only the path to this note
this.spotlightedNotePath = notePath;
await parentNode.load(true);
// Try to find the child again after reload
foundChildNode = this.findChildNode(parentNode, childNoteId);
this.spotlightedNode = foundChildNode ?? null;
if (!foundChildNode) {
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
}
parentNode = foundChildNode;
continue;
}
// besides real errors, this can be also caused by hiding of e.g. included images
// these are real notes with real notePath, user can display them in a detail,
// but they don't have a node in the tree
if (logErrors || !childNote) {
ws.logError(
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
);
return;
}
return;
@@ -1079,7 +1066,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
return parentNode.getChildren()?.find((childNode) => childNode.data.noteId === childNoteId);
}
async expandToNote(notePath: string, logErrors = true) {
@@ -1120,12 +1107,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
(!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || (await hoistedNoteService.isHoistedInHiddenSubtree())) &&
(await this.getNodeFromPath(this.noteContext.notePath));
if (this.spotlightedNode && newActiveNode !== this.spotlightedNode) {
// Can get removed when switching to another note in a spotlighted subtree.
if (this.spotlightedNode.parent) {
this.spotlightedNode.remove();
}
this.spotlightedNode = null;
this.spotlightedNotePath = null;
}
if (newActiveNode !== oldActiveNode) {
let oldActiveNodeFocused = false;
if (oldActiveNode) {
oldActiveNodeFocused = oldActiveNode.hasFocus();
oldActiveNode.setActive(false);
oldActiveNode.setFocus(false);
}
@@ -1228,10 +1223,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
for (const noteId of loadResults.getNoteIds()) {
const contentReloaded = loadResults.isNoteContentReloaded(noteId);
if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) {
// Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note.
continue;
}
refreshCtx.noteIdsToUpdate.add(noteId);
}
await this.#executeTreeUpdates(refreshCtx, loadResults);
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
await this.#executeTreeUpdates(refreshCtx, loadResults);
}
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
@@ -1251,7 +1254,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else {
refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
}
} else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
} else if (attrRow.type === "label" && (attrRow.name === "archived" || attrRow.name === "subtreeHidden") && attrRow.noteId) {
const note = froca.getNoteFromCache(attrRow.noteId);
if (note) {
@@ -1336,18 +1339,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
} else if (frocaBranch) {
// make sure it's loaded
// we're forcing lazy since it's not clear if the whole required subtree is in froca
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (!parentNode.data.subtreeHidden) {
const newNode = this.prepareNode(frocaBranch, true);
if (newNode) {
parentNode.addChildren([newNode]);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
}
this.sortChildren(parentNode);
this.sortChildren(parentNode);
} else if (branchRow.componentId === this.componentId) {
// Display the toast and focus to parent note only if we know for sure that the operation comes from the tree.
const parentNote = froca.getNoteFromCache(parentNode.data.noteId || "");
toastService.showPersistent({
id: `subtree-hidden-moved`,
title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }),
message: parentNote?.type === "book"
? t("note_tree.subtree-hidden-moved-description-collection")
: t("note_tree.subtree-hidden-moved-description-other"),
icon: "bx bx-hide",
timeout: 5_000,
});
parentNode.setActive(true);
}
// this might be a first child which would force an icon change
// also update the count if the subtree is hidden.
if (branchRow.parentNoteId) {
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
}
@@ -1363,7 +1382,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
await this.batchUpdate(async () => {
const performUpdates = async () => {
for (const noteId of refreshCtx.noteIdsToReload) {
for (const node of this.getNodesByNoteId(noteId)) {
await node.load(true);
@@ -1379,7 +1398,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
}
}
});
};
if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) {
/**
* Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title)
* it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates.
* Without batching, the updates would take a couple of milliseconds.
* We still keep the batching for potential cases where there are many updates, for example in a sync.
*/
await this.batchUpdate(performUpdates);
} else {
await performUpdates();
}
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
for (const noteId of refreshCtx.noteIdsToUpdate) {
@@ -1643,7 +1674,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const toNode = node.getPrevSibling();
if (toNode !== null) {
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId);
}
}
@@ -1780,12 +1811,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
if (desktopLaunchersToMove) {
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId);
}
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
if (mobileLaunchersToMove) {
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId);
}
}
@@ -1853,3 +1884,112 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
return items;
}
}
function buildEnhanceTitle() {
const createChildTemplate = document.createElement("span");
createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus";
createChildTemplate.title = t("note_tree.create-child-note");
return async function enhanceTitle(event: Event,
data: {
node: Fancytree.FancytreeNode;
noteId: string;
}) {
const node = data.node;
if (!node.data.noteId) {
// if there's "non-note" node, then don't enhance
// this can happen for e.g. "Load error!" node
return;
}
const note = froca.getNoteFromCache(node.data.noteId);
if (!note) return;
const activeNoteContext = appContext.tabManager.getActiveContext();
const $span = $(node.span);
$span.find(".tree-item-button").remove();
$span.find(".note-indicator-icon").remove();
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
if (note.hasLabel("workspace") && !isHoistedNote) {
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($enterWorkspaceButton);
}
if (note.type === "search") {
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
"click",
cancelClickPropagation
);
$span.append($refreshSearchButton);
}
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
const isSubtreeHidden = note.isLabelTruthy("subtreeHidden");
if (!["search", "launcher"].includes(note.type)
&& !note.isOptions()
&& !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help")
&& !isSubtreeHidden
&& !node.extraClasses.includes("spotlighted-node")
) {
const createChildItem = createChildTemplate.cloneNode();
createChildItem.addEventListener("click", cancelClickPropagation);
node.span.append(createChildItem);
}
if (isHoistedNote) {
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
$span.append($unhoistButton);
}
// Add clone indicator with tooltip if note has multiple parents
const parentNotes = note.getParentNotes();
const realParents: FNote[] = [];
for (const parent of parentNotes) {
if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") {
realParents.push(parent);
}
}
if (realParents.length > 1) {
const parentTitles = realParents.map((p) => p.title).join(", ");
const tooltipText = realParents.length === 2
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
$cloneIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($cloneIndicator);
}
// Add shared indicator with tooltip if note is shared
if (note.isShared()) {
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
$sharedIndicator.attr("title", tooltipText);
$span.find(".fancytree-title").append($sharedIndicator);
}
// Add a badge with the number of items if it hides children.
const count = note.getChildNoteIds().length;
if (isSubtreeHidden && count > 0) {
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
$span.find(".fancytree-title").append($badge);
}
};
}

View File

@@ -4,7 +4,8 @@
*/
import { NoteType } from "@triliumnext/commons";
import { VNode, type JSX } from "preact";
import { type JSX, VNode } from "preact";
import { TypeWidgetProps } from "./type_widgets/type_widget";
/**
@@ -13,7 +14,7 @@ import { TypeWidgetProps } from "./type_widgets/type_widget";
*/
export type ExtendedNoteType = Exclude<NoteType, "launcher" | "text" | "code"> | "empty" | "readOnlyCode" | "readOnlyText" | "editableText" | "editableCode" | "attachmentDetail" | "attachmentList" | "protectedSession" | "aiChat";
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element);
export type TypeWidget = ((props: TypeWidgetProps) => VNode | JSX.Element | undefined);
type NoteTypeView = () => (Promise<{ default: TypeWidget } | TypeWidget> | TypeWidget);
interface NoteTypeMapping {

View File

@@ -64,7 +64,8 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`);
this.$widget.addClass(note.getColorClass());
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
this.$widget.toggleClass("options", note.isOptions());
this.$widget.toggleClass("bgfx", this.#hasBackgroundEffects(note));
this.$widget.toggleClass("protected", note.isProtected);
const noteLanguage = note?.getLabelValue("language");
@@ -88,6 +89,22 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
return !!note?.isLabelTruthy("fullContentWidth");
}
#hasBackgroundEffects(note: FNote): boolean {
const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [
"application/pdf"
]
if (note.isOptions()) {
return true;
}
if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) {
return true;
}
return false;
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
// listening on changes of note.type and CSS class
const LABELS_CAUSING_REFRESH = ["cssClass", "language", "viewType", "color"];

View File

@@ -8,7 +8,7 @@ import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayou
import appContext, { EventData, EventNames } from "../../components/app_context";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
import NoteContext, { NoteContextDataMap } from "../../components/note_context";
import FBlob from "../../entities/fblob";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
@@ -19,7 +19,7 @@ import options, { type OptionValue } from "../../services/options";
import protected_session_holder from "../../services/protected_session_holder";
import server from "../../services/server";
import shortcuts, { Handler, removeIndividualBinding } from "../../services/shortcuts";
import SpacedUpdate from "../../services/spaced_update";
import SpacedUpdate, { type StateCallback } from "../../services/spaced_update";
import toast, { ToastOptions } from "../../services/toast";
import tree from "../../services/tree";
import utils, { escapeRegExp, getErrorMessage, randomString, reloadFrontendApp } from "../../services/utils";
@@ -63,22 +63,29 @@ export function useTriliumEvents<T extends EventNames>(eventNames: T[], handler:
useDebugValue(() => eventNames.join(", "));
}
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000) {
export function useSpacedUpdate(callback: () => void | Promise<void>, interval = 1000, stateCallback?: StateCallback) {
const callbackRef = useRef(callback);
const stateCallbackRef = useRef(stateCallback);
const spacedUpdateRef = useRef<SpacedUpdate>(new SpacedUpdate(
() => callbackRef.current(),
interval
interval,
(state) => stateCallbackRef.current?.(state)
));
// Update callback ref when it changes
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
}, [ callback ]);
// Update state callback when it changes.
useEffect(() => {
stateCallbackRef.current = stateCallback;
}, [ stateCallback ]);
// Update interval if it changes
useEffect(() => {
spacedUpdateRef.current?.setUpdateInterval(interval);
}, [interval]);
}, [ interval ]);
return spacedUpdateRef.current;
}
@@ -121,7 +128,12 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent ]);
const spacedUpdate = useSpacedUpdate(callback);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
@@ -158,6 +170,73 @@ export function useEditorSpacedUpdate({ note, noteType, noteContext, getData, on
return spacedUpdate;
}
export function useBlobEditorSpacedUpdate({ note, noteType, noteContext, getData, onContentChange, dataSaved, updateInterval, replaceWithoutRevision }: {
noteType: NoteType;
note: FNote,
noteContext: NoteContext | null | undefined,
getData: () => Promise<Blob | undefined> | Blob | undefined,
onContentChange: (newBlob: FBlob) => void,
dataSaved?: (savedData: Blob) => void,
updateInterval?: number;
/** If set to true, then the blob is replaced directly without saving a revision before. */
replaceWithoutRevision?: boolean;
}) {
const parentComponent = useContext(ParentComponent);
const blob = useNoteBlob(note, parentComponent?.componentId);
const callback = useMemo(() => {
return async () => {
const data = await getData();
// for read only notes
if (data === undefined || note.type !== noteType) return;
protected_session_holder.touchProtectedSessionIfNecessary(note);
await server.upload(`notes/${note.noteId}/file?replace=${replaceWithoutRevision ? "1" : "0"}`, new File([ data ], note.title, { type: note.mime }), parentComponent?.componentId);
dataSaved?.(data);
};
}, [ note, getData, dataSaved, noteType, parentComponent, replaceWithoutRevision ]);
const stateCallback = useCallback<StateCallback>((state) => {
noteContext?.setContextData("saveState", {
state
});
}, [ noteContext ]);
const spacedUpdate = useSpacedUpdate(callback, updateInterval, stateCallback);
// React to note/blob changes.
useEffect(() => {
if (!blob) return;
spacedUpdate.allowUpdateWithoutChange(() => onContentChange(blob));
}, [ blob ]);
// React to update interval changes.
useEffect(() => {
if (!updateInterval) return;
spacedUpdate.setUpdateInterval(updateInterval);
}, [ updateInterval ]);
// Save if needed upon switching tabs.
useTriliumEvent("beforeNoteSwitch", async ({ noteContext: eventNoteContext }) => {
if (eventNoteContext.ntxId !== noteContext?.ntxId) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon tab closing.
useTriliumEvent("beforeNoteContextRemove", async ({ ntxIds }) => {
if (!noteContext?.ntxId || !ntxIds.includes(noteContext.ntxId)) return;
await spacedUpdate.updateNowIfNecessary();
});
// Save if needed upon window/browser closing.
useEffect(() => {
const listener = () => spacedUpdate.isAllSavedAndTriggerUpdate();
appContext.addBeforeUnloadListener(listener);
return () => appContext.removeBeforeUnloadListener(listener);
}, []);
return spacedUpdate;
}
export function useNoteSavedData(noteId: string | undefined) {
return useSyncExternalStore(
(cb) => noteId ? noteSavedDataStore.subscribe(noteId, cb) : () => {},
@@ -567,17 +646,13 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
const setter = useCallback((value: boolean) => {
if (note) {
if (value) {
attributes.setLabel(note.noteId, labelName, "");
} else {
attributes.removeOwnedLabelByName(note, labelName);
}
attributes.setBooleanWithInheritance(note, labelName, value);
}
}, [note]);
}, [note, labelName]);
useDebugValue(labelName);
const labelValue = !!note?.hasLabel(labelName);
const labelValue = !!note?.isLabelTruthy(labelName);
return [ labelValue, setter ] as const;
}
@@ -634,7 +709,8 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
const ref = useRef<HTMLDivElement>(null);
const parentComponent = useContext(ParentComponent);
// Render the widget once.
// Render the widget once - note that noteContext is intentionally NOT a dependency
// to prevent creating new widget instances on every note switch.
const [ widget, renderedWidget ] = useMemo(() => {
const widget = widgetFactory();
@@ -642,14 +718,21 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
parentComponent.child(widget);
}
if (noteContext && widget instanceof NoteContextAwareWidget) {
widget.setNoteContextEvent({ noteContext });
}
const renderedWidget = widget.render();
return [ widget, renderedWidget ];
}, [ noteContext, parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
// widgetFactory() is intentionally left out
}, [ parentComponent ]); // eslint-disable-line react-hooks/exhaustive-deps
// widgetFactory() and noteContext are intentionally left out - widget should be created once
// and updated via activeContextChangedEvent when noteContext changes.
// Cleanup: remove widget from parent's children when unmounted
useEffect(() => {
return () => {
if (parentComponent) {
parentComponent.removeChild(widget);
}
widget.cleanup();
};
}, [ parentComponent, widget ]);
// Attach the widget to the parent.
useEffect(() => {
@@ -660,10 +743,17 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
}
}, [ renderedWidget ]);
// Inject the note context.
// Inject the note context - this updates the existing widget without recreating it.
// We check if the context actually changed to avoid double refresh when the event system
// also delivers activeContextChanged to the widget through component tree propagation.
useEffect(() => {
if (noteContext && widget instanceof NoteContextAwareWidget) {
widget.activeContextChangedEvent({ noteContext });
// Only trigger refresh if the context actually changed.
// The event system may have already updated the widget, in which case
// widget.noteContext will already equal noteContext.
if (widget.noteContext !== noteContext) {
widget.activeContextChangedEvent({ noteContext });
}
}
}, [ noteContext, widget ]);
@@ -1192,3 +1282,92 @@ export function useContentElement(noteContext: NoteContext | null | undefined) {
return contentElement;
}
/**
* Set context data on the current note context.
* This allows type widgets to publish data (e.g., table of contents, PDF pages)
* that can be consumed by sidebar/toolbar components.
*
* Data is automatically cleared when navigating to a different note.
*
* @param key - Unique identifier for the data type (e.g., "toc", "pdfPages")
* @param value - The data to publish
*
* @example
* // In a PDF viewer widget:
* const { noteContext } = useActiveNoteContext();
* useSetContextData(noteContext, "pdfPages", pages);
*/
export function useSetContextData<K extends keyof NoteContextDataMap>(
noteContext: NoteContext | null | undefined,
key: K,
value: NoteContextDataMap[K] | undefined
) {
useEffect(() => {
if (!noteContext) return;
if (value !== undefined) {
noteContext.setContextData(key, value);
} else {
noteContext.clearContextData(key);
}
return () => {
noteContext.clearContextData(key);
};
}, [noteContext, key, value]);
}
/**
* Get context data from the active note context.
* This is typically used in sidebar/toolbar components that need to display
* data published by type widgets.
*
* The component will automatically re-render when the data changes.
*
* @param key - The data key to retrieve (e.g., "toc", "pdfPages")
* @returns The current data, or undefined if not available
*
* @example
* // In a Table of Contents sidebar widget:
* function TableOfContents() {
* const headings = useGetContextData<Heading[]>("toc");
* if (!headings) return <div>No headings available</div>;
* return <ul>{headings.map(h => <li>{h.text}</li>)}</ul>;
* }
*/
export function useGetContextData<K extends keyof NoteContextDataMap>(key: K): NoteContextDataMap[K] | undefined {
const { noteContext } = useActiveNoteContext();
return useGetContextDataFrom(noteContext, key);
}
/**
* Get context data from a specific note context (not necessarily the active one).
*
* @param noteContext - The specific note context to get data from
* @param key - The data key to retrieve
* @returns The current data, or undefined if not available
*/
export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
noteContext: NoteContext | null | undefined,
key: K
): NoteContextDataMap[K] | undefined {
const [data, setData] = useState<NoteContextDataMap[K] | undefined>(() =>
noteContext?.getContextData(key)
);
// Update initial value when noteContext changes
useEffect(() => {
setData(noteContext?.getContextData(key));
}, [noteContext, key]);
// Subscribe to changes via Trilium event system
useTriliumEvent("contextDataChanged", ({ noteContext: eventNoteContext, key: changedKey, value }) => {
if (eventNoteContext === noteContext && changedKey === key) {
setData(value as NoteContextDataMap[K]);
}
});
return data;
}

View File

@@ -1,3 +1,5 @@
import { useContext } from "preact/hooks";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { downloadFileNote, openNoteExternally } from "../../services/open";
@@ -8,11 +10,14 @@ import { formatSize } from "../../services/utils";
import Button from "../react/Button";
import { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
export default function FilePropertiesTab({ note, ntxId }: Pick<TabContext, "note" | "ntxId">) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
const blob = useNoteBlob(note);
const parentComponent = useContext(ParentComponent);
return (
<div className="file-properties-widget">
@@ -40,7 +45,7 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
text={t("file_properties.download")}
primary
disabled={!canAccessProtectedNote}
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
<Button

View File

@@ -44,7 +44,7 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
text={t("image_properties.download")}
icon="bx bx-download"
primary
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
<Button

View File

@@ -135,13 +135,13 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
);
}
function DownloadFileButton({ note }: NoteActionsCustomInnerProps) {
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
return (
<ActionButton
icon="bx bx-download"
text={t("file_properties.download")}
disabled={!note.isContentAvailable()}
onClick={() => downloadFileNote(note.noteId)}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>
);
}

View File

@@ -3,7 +3,7 @@ import "./RightPanelContainer.css";
import Split from "@triliumnext/split.js";
import { VNode } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { useState, useEffect, useRef, useCallback } from "preact/hooks";
import appContext from "../../components/app_context";
import { WidgetsByParent } from "../../services/bundle";
@@ -11,10 +11,13 @@ import { t } from "../../services/i18n";
import options from "../../services/options";
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
import Button from "../react/Button";
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionBool, useTriliumOptionJson } from "../react/hooks";
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumEvent, useTriliumOptionJson } from "../react/hooks";
import Icon from "../react/Icon";
import LegacyRightPanelWidget from "../right_panel_widget";
import HighlightsList from "./HighlightsList";
import PdfAttachments from "./pdf/PdfAttachments";
import PdfLayers from "./pdf/PdfLayers";
import PdfPages from "./pdf/PdfPages";
import RightPanelWidget from "./RightPanelWidget";
import TableOfContents from "./TableOfContents";
@@ -27,12 +30,16 @@ interface RightPanelWidgetDefinition {
}
export default function RightPanelContainer({ widgetsByParent }: { widgetsByParent: WidgetsByParent }) {
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
const [ rightPaneVisible, setRightPaneVisible ] = useState(options.is("rightPaneVisible"));
const items = useItems(rightPaneVisible, widgetsByParent);
useSplit(rightPaneVisible);
useTriliumEvent("toggleRightPane", () => {
setRightPaneVisible(!rightPaneVisible);
});
useTriliumEvent("toggleRightPane", useCallback(() => {
setRightPaneVisible(current => {
const newValue = !current;
options.save("rightPaneVisible", newValue.toString());
return newValue;
});
}, []));
return (
<div id="right-pane">
@@ -45,7 +52,7 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
{t("right_pane.empty_message")}
<Button
text={t("right_pane.empty_button")}
onClick={() => setRightPaneVisible(!rightPaneVisible)}
triggerCommand="toggleRightPane"
/>
</div>
)
@@ -57,13 +64,27 @@ export default function RightPanelContainer({ widgetsByParent }: { widgetsByPare
function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
const isPdf = noteType === "file" && noteMime === "application/pdf";
if (!rightPaneVisible) return [];
const definitions: RightPanelWidgetDefinition[] = [
{
el: <TableOfContents />,
enabled: (noteType === "text" || noteType === "doc"),
enabled: (noteType === "text" || noteType === "doc" || isPdf),
},
{
el: <PdfPages />,
enabled: isPdf,
},
{
el: <PdfAttachments />,
enabled: isPdf,
},
{
el: <PdfLayers />,
enabled: isPdf,
},
{
el: <HighlightsList />,

View File

@@ -29,6 +29,11 @@
hyphens: auto;
}
.toc li.active > .item-content {
font-weight: bold;
color: var(--main-text-color);
}
.toc > ol {
--toc-depth-level: 1;
}

View File

@@ -2,12 +2,14 @@ import "./TableOfContents.css";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import math from "../../services/math";
import { randomString } from "../../services/utils";
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import { useActiveNoteContext, useContentElement, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
import Icon from "../react/Icon";
import RawHtml from "../react/RawHtml";
import RightPanelWidget from "./RightPanelWidget";
//#region Generic impl.
@@ -21,29 +23,50 @@ interface HeadingsWithNesting extends RawHeading {
children: HeadingsWithNesting[];
}
export interface HeadingContext {
scrollToHeading(heading: RawHeading): void;
headings: RawHeading[];
activeHeadingId?: string | null;
}
export default function TableOfContents() {
const { note, noteContext } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
return (
<RightPanelWidget id="toc" title={t("toc.table_of_contents")} grow>
{((noteType === "text" && isReadOnly) || (noteType === "doc")) && <ReadOnlyTextTableOfContents />}
{noteType === "text" && !isReadOnly && <EditableTextTableOfContents />}
{noteType === "file" && noteMime === "application/pdf" && <PdfTableOfContents />}
</RightPanelWidget>
);
}
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading }: {
function PdfTableOfContents() {
const data = useGetContextData("toc");
return (
<AbstractTableOfContents
headings={data?.headings || []}
scrollToHeading={data?.scrollToHeading || (() => {})}
activeHeadingId={data?.activeHeadingId}
/>
);
}
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading, activeHeadingId }: {
headings: T[];
scrollToHeading(heading: T): void;
activeHeadingId?: string | null;
}) {
const nestedHeadings = buildHeadingTree(headings);
return (
<span className="toc">
{nestedHeadings.length > 0 ? (
<ol>
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
) : (
<div className="no-headings">{t("toc.no_headings")}</div>
@@ -52,14 +75,32 @@ function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeadi
);
}
function TableOfContentsHeading({ heading, scrollToHeading }: {
function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
heading: HeadingsWithNesting;
scrollToHeading(heading: RawHeading): void;
activeHeadingId?: string | null;
}) {
const [ collapsed, setCollapsed ] = useState(false);
const isActive = heading.id === activeHeadingId;
const contentRef = useRef<HTMLElement>(null);
// Render math equations after component mounts/updates
useEffect(() => {
if (!contentRef.current) return;
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
for (const mathEl of mathElements ?? []) {
try {
math.render(mathEl.textContent || "", mathEl as HTMLElement);
} catch (e) {
console.warn("Failed to render math in TOC:", e);
}
}
}, [heading.text]);
return (
<>
<li className={clsx(collapsed && "collapsed")}>
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
{heading.children.length > 0 && (
<Icon
className="collapse-button"
@@ -67,14 +108,16 @@ function TableOfContentsHeading({ heading, scrollToHeading }: {
onClick={() => setCollapsed(!collapsed)}
/>
)}
<span
<RawHtml
containerRef={contentRef}
className="item-content"
onClick={() => scrollToHeading(heading)}
>{heading.text}</span>
html={heading.text}
/>
</li>
{heading.children && (
{heading.children.length > 0 && (
<ol>
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
</ol>
)}
</>
@@ -166,9 +209,23 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
const level = Number(item.name.replace( 'heading', '' ));
const text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
// Convert model element to view, then to DOM to get HTML
const viewEl = editor.editing.mapper.toViewElement(item);
let text = '';
if (viewEl) {
const domEl = editor.editing.view.domConverter.mapViewToDom(viewEl);
if (domEl instanceof HTMLElement) {
text = domEl.innerHTML;
}
}
// Fallback to plain text if conversion fails
if (!text) {
text = Array.from( item.getChildren() )
.map( c => c.is( '$text' ) ? c.data : '' )
.join( '' );
}
// Assign a unique ID
let tocId = item.getAttribute(TOC_ID) as string | undefined;

View File

@@ -0,0 +1,57 @@
.pdf-attachments-list {
width: 100%;
}
.pdf-attachment-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--main-border-color);
transition: background-color 0.2s;
}
.pdf-attachment-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-attachment-item:last-child {
border-bottom: none;
}
.pdf-attachment-info {
flex: 1;
min-width: 0;
}
.pdf-attachment-filename {
font-size: 13px;
font-weight: 500;
color: var(--main-text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pdf-attachment-size {
font-size: 11px;
color: var(--muted-text-color);
margin-top: 2px;
}
.no-attachments {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}
.pdf-attachment-item .bx {
flex-shrink: 0;
font-size: 18px;
color: var(--muted-text-color);
}
.pdf-attachment-item:hover .bx {
color: var(--main-text-color);
}

View File

@@ -0,0 +1,62 @@
import "./PdfAttachments.css";
import { t } from "../../../services/i18n";
import { formatSize } from "../../../services/utils";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import Icon from "../../react/Icon";
import RightPanelWidget from "../RightPanelWidget";
interface AttachmentInfo {
filename: string;
size: number;
}
export default function PdfAttachments() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const attachmentsData = useGetContextData("pdfAttachments");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
if (!attachmentsData || attachmentsData.attachments.length === 0) {
return null;
}
return (
<RightPanelWidget id="pdf-attachments" title={t("pdf.attachments", { count: attachmentsData.attachments.length })}>
<div className="pdf-attachments-list">
{attachmentsData.attachments.map((attachment) => (
<PdfAttachmentItem
key={attachment.filename}
attachment={attachment}
onDownload={attachmentsData.downloadAttachment}
/>
))}
</div>
</RightPanelWidget>
);
}
function PdfAttachmentItem({
attachment,
onDownload
}: {
attachment: AttachmentInfo;
onDownload: (filename: string) => void;
}) {
const sizeText = formatSize(attachment.size);
return (
<div className="pdf-attachment-item" onClick={() => onDownload(attachment.filename)}>
<Icon icon="bx bx-paperclip" />
<div className="pdf-attachment-info">
<div className="pdf-attachment-filename">{attachment.filename}</div>
<div className="pdf-attachment-size">{sizeText}</div>
</div>
<Icon icon="bx bx-download" />
</div>
);
}

View File

@@ -0,0 +1,54 @@
.pdf-layers-list {
width: 100%;
}
.pdf-layer-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--main-border-color);
transition: background-color 0.2s;
}
.pdf-layer-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-layer-item:last-child {
border-bottom: none;
}
.pdf-layer-item.hidden {
opacity: 0.5;
}
.pdf-layer-name {
flex: 1;
font-size: 13px;
color: var(--main-text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-layers {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}
.pdf-layer-item .bx {
flex-shrink: 0;
font-size: 18px;
color: var(--muted-text-color);
}
.pdf-layer-item:hover .bx {
color: var(--main-text-color);
}
.pdf-layer-item.visible .bx {
color: var(--main-text-color);
}

View File

@@ -0,0 +1,55 @@
import "./PdfLayers.css";
import { t } from "../../../services/i18n";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import Icon from "../../react/Icon";
import RightPanelWidget from "../RightPanelWidget";
interface LayerInfo {
id: string;
name: string;
visible: boolean;
}
export default function PdfLayers() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const layersData = useGetContextData("pdfLayers");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
return (layersData?.layers && layersData.layers.length > 0 &&
<RightPanelWidget id="pdf-layers" title={t("pdf.layers", { count: layersData.layers.length })}>
<div className="pdf-layers-list">
{layersData.layers.map((layer) => (
<PdfLayerItem
key={layer.id}
layer={layer}
onToggle={layersData.toggleLayer}
/>
))}
</div>
</RightPanelWidget>
);
}
function PdfLayerItem({
layer,
onToggle
}: {
layer: LayerInfo;
onToggle: (layerId: string, visible: boolean) => void;
}) {
return (
<div
className={`pdf-layer-item ${layer.visible ? 'visible' : 'hidden'}`}
onClick={() => onToggle(layer.id, !layer.visible)}
>
<Icon icon={layer.visible ? "bx bx-show" : "bx bx-hide"} />
<div className="pdf-layer-name">{layer.name}</div>
</div>
);
}

View File

@@ -0,0 +1,67 @@
.pdf-pages-list {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
padding: 8px;
align-content: flex-start;
}
.pdf-page-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: border-color 0.2s;
box-sizing: border-box;
position: relative;
.pdf-page-number {
font-size: 12px;
margin-bottom: 4px;
color: var(--main-text-color);
position: absolute;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
background-color: var(--accented-background-color);
padding: 0.2em 0.5em;
border-radius: 4px;
}
}
.pdf-page-item:hover {
background-color: var(--hover-item-background-color);
}
.pdf-page-item.active {
border-color: var(--main-border-color);
background-color: var(--active-item-background-color);
}
.pdf-page-thumbnail {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.pdf-page-thumbnail img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.pdf-page-loading {
color: var(--muted-text-color);
font-size: 11px;
}
.no-pages {
padding: 16px;
text-align: center;
color: var(--muted-text-color);
}

View File

@@ -0,0 +1,111 @@
import "./PdfPages.css";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { NoteContextDataMap } from "../../../components/note_context";
import { t } from "../../../services/i18n";
import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../../react/hooks";
import RightPanelWidget from "../RightPanelWidget";
export default function PdfPages() {
const { note } = useActiveNoteContext();
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
const pagesData = useGetContextData("pdfPages");
if (noteType !== "file" || noteMime !== "application/pdf") {
return null;
}
return (pagesData &&
<RightPanelWidget id="pdf-pages" title={t("pdf.pages", { count: pagesData?.totalPages || 0 })} grow>
<PdfPagesList key={note?.noteId} pagesData={pagesData} />
</RightPanelWidget>
);
}
function PdfPagesList({ pagesData }: { pagesData: NoteContextDataMap["pdfPages"] }) {
const [thumbnails, setThumbnails] = useState<Map<number, string>>(new Map());
const requestedThumbnails = useRef<Set<number>>(new Set());
useEffect(() => {
// Listen for thumbnail responses via custom event
function handleThumbnail(event: CustomEvent) {
const { pageNumber, dataUrl } = event.detail;
setThumbnails(prev => new Map(prev).set(pageNumber, dataUrl));
}
window.addEventListener("pdf-thumbnail", handleThumbnail as EventListener);
return () => {
window.removeEventListener("pdf-thumbnail", handleThumbnail as EventListener);
};
}, []);
const requestThumbnail = useCallback((pageNumber: number) => {
// Only request if we haven't already requested it and don't have it
if (!requestedThumbnails.current.has(pageNumber) && !thumbnails.has(pageNumber) && pagesData) {
requestedThumbnails.current.add(pageNumber);
pagesData.requestThumbnail(pageNumber);
}
}, [pagesData, thumbnails]);
if (!pagesData || pagesData.totalPages === 0) {
return <div className="no-pages">No pages available</div>;
}
const pages = Array.from({ length: pagesData.totalPages }, (_, i) => i + 1);
return (
<div className="pdf-pages-list">
{pages.map(pageNumber => (
<PdfPageItem
key={pageNumber}
pageNumber={pageNumber}
isActive={pageNumber === pagesData.currentPage}
thumbnail={thumbnails.get(pageNumber)}
onRequestThumbnail={requestThumbnail}
onPageClick={() => pagesData.scrollToPage(pageNumber)}
/>
))}
</div>
);
}
function PdfPageItem({
pageNumber,
isActive,
thumbnail,
onRequestThumbnail,
onPageClick
}: {
pageNumber: number;
isActive: boolean;
thumbnail?: string;
onRequestThumbnail(page: number): void;
onPageClick(): void;
}) {
const hasRequested = useRef(false);
useEffect(() => {
if (!thumbnail && !hasRequested.current) {
hasRequested.current = true;
onRequestThumbnail(pageNumber);
}
}, [pageNumber, thumbnail, onRequestThumbnail]);
return (
<div
className={`pdf-page-item ${isActive ? 'active' : ''}`}
onClick={onPageClick}
>
<div className="pdf-page-number">{pageNumber}</div>
<div className="pdf-page-thumbnail">
{thumbnail ? (
<img src={thumbnail} alt={t("pdf.pages_alt", { pageNumber })} />
) : (
<div className="pdf-page-loading">{t("pdf.pages_loading")}</div>
)}
</div>
</div>
);
}

View File

@@ -23,7 +23,7 @@ export default function SqlResults() {
{t("sql_result.no_rows")}
</Alert>
) : (
<div class="sql-console-result-container">
<div className="sql-console-result-container selectable-text">
{results?.map(rows => {
// inserts, updates
if (typeof rows === "object" && !Array.isArray(rows)) {

View File

@@ -1,27 +1,29 @@
import { useNoteBlob } from "../react/hooks";
import "./File.css";
import { TypeWidgetProps } from "./type_widget";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getUrlForDownload } from "../../services/open";
import Alert from "../react/Alert";
import { t } from "../../services/i18n";
import { useNoteBlob } from "../react/hooks";
import PdfPreview from "./file/Pdf";
import { TypeWidgetProps } from "./type_widget";
const TEXT_MAX_NUM_CHARS = 5000;
export default function File({ note }: TypeWidgetProps) {
const blob = useNoteBlob(note);
export default function FileTypeWidget({ note, parentComponent, noteContext }: TypeWidgetProps) {
const blob = useNoteBlob(note, parentComponent?.componentId);
if (blob?.content) {
return <TextPreview content={blob.content} />
return <TextPreview content={blob.content} />;
} else if (note.mime === "application/pdf") {
return <PdfPreview note={note} />
return noteContext && <PdfPreview blob={blob} note={note} componentId={parentComponent?.componentId} noteContext={noteContext} />;
} else if (note.mime.startsWith("video/")) {
return <VideoPreview note={note} />
return <VideoPreview note={note} />;
} else if (note.mime.startsWith("audio/")) {
return <AudioPreview note={note} />
} else {
return <NoPreview />
return <AudioPreview note={note} />;
}
return <NoPreview />;
}
function TextPreview({ content }: { content: string }) {
@@ -37,14 +39,6 @@ function TextPreview({ content }: { content: string }) {
)}
<pre class="file-preview-content">{trimmedContent}</pre>
</>
)
}
function PdfPreview({ note }: { note: FNote }) {
return (
<iframe
class="pdf-preview"
src={getUrlForDownload(`api/notes/${note.noteId}/open`)} />
);
}
@@ -56,7 +50,7 @@ function VideoPreview({ note }: { note: FNote }) {
datatype={note?.mime}
controls
/>
)
);
}
function AudioPreview({ note }: { note: FNote }) {
@@ -66,7 +60,7 @@ function AudioPreview({ note }: { note: FNote }) {
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
controls
/>
)
);
}
function NoPreview() {

View File

@@ -29,6 +29,7 @@ export default function Mermaid(props: TypeWidgetProps) {
<SvgSplitEditor
attachmentName="mermaid-export"
renderSvg={renderSvg}
noteType="mermaid"
{...props}
/>
);

View File

@@ -37,6 +37,7 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
it: "it",
ja: "ja",
pt: "pt",
pl: null,
pt_br: "pt",
ro: "ro",
ru: "ru",

View File

@@ -13,6 +13,7 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
it: "it-IT",
ja: "ja-JP",
pt: "pt-PT",
pl: "pl-PL",
pt_br: "pt-BR",
ro: "ro-RO",
ru: "ru-RU",

View File

@@ -1,6 +1,7 @@
import "./code.css";
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
import { NoteType } from "@triliumnext/commons";
import { useEffect, useRef, useState } from "preact/hooks";
import appContext, { CommandListenerData } from "../../../components/app_context";
@@ -24,6 +25,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
debounceUpdate?: boolean;
lineWrapping?: boolean;
updateInterval?: number;
noteType?: NoteType;
/** Invoked when the content of the note is changed, such as a different revision or a note switch. */
onContentChanged?: (content: string) => void;
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
@@ -72,14 +74,14 @@ function formatViewSource(note: FNote, content: string) {
return content;
}
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, ...editorProps }: EditableCodeProps) {
const editorRef = useRef<VanillaCodeMirror>(null);
const containerRef = useRef<HTMLPreElement>(null);
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
const mime = useNoteProperty(note, "mime");
const spacedUpdate = useEditorSpacedUpdate({
note,
noteType: "code",
noteType,
noteContext,
getData: () => ({ content: editorRef.current?.getText() ?? "" }),
onContentChange: (content) => {

View File

@@ -0,0 +1,234 @@
import { useEffect, useRef } from "preact/hooks";
import appContext from "../../../components/app_context";
import type NoteContext from "../../../components/note_context";
import FBlob from "../../../entities/fblob";
import FNote from "../../../entities/fnote";
import { useViewModeConfig } from "../../collections/NoteList";
import { useBlobEditorSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import PdfViewer from "./PdfViewer";
export default function PdfPreview({ note, blob, componentId, noteContext }: {
note: FNote;
noteContext: NoteContext;
blob: FBlob | null | undefined;
componentId: string | undefined;
}) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const historyConfig = useViewModeConfig<HistoryData>(note, "pdfHistory");
const spacedUpdate = useBlobEditorSpacedUpdate({
note,
noteType: "file",
noteContext,
getData() {
if (!iframeRef.current?.contentWindow) return undefined;
return new Promise<Blob>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout while waiting for blob response"));
}, 10_000);
const onMessageReceived = (event: PdfMessageEvent) => {
if (event.data.type !== "pdfjs-viewer-blob") return;
if (event.data.noteId !== note.noteId || event.data.ntxId !== noteContext.ntxId) return;
const blob = new Blob([event.data.data as Uint8Array<ArrayBuffer>], { type: note.mime });
clearTimeout(timeout);
window.removeEventListener("message", onMessageReceived);
resolve(blob);
};
window.addEventListener("message", onMessageReceived);
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-blob",
}, window.location.origin);
});
},
onContentChange() {
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.location.reload();
}
},
replaceWithoutRevision: true
});
useEffect(() => {
function handleMessage(event: PdfMessageEvent) {
if (event.data?.type === "pdfjs-viewer-document-modified") {
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
spacedUpdate.resetUpdateTimer();
spacedUpdate.scheduleUpdate();
}
}
if (event.data.type === "pdfjs-viewer-save-view-history" && event.data?.data) {
if (event.data.noteId === note.noteId && event.data.ntxId === noteContext.ntxId) {
historyConfig?.storeFn(JSON.parse(event.data.data));
}
}
if (event.data.type === "pdfjs-viewer-toc") {
if (event.data.data) {
// Convert PDF outline to HeadingContext format
const headings = convertPdfOutlineToHeadings(event.data.data);
noteContext.setContextData("toc", {
headings,
activeHeadingId: null,
scrollToHeading: (heading) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-scroll-to-heading",
headingId: heading.id
}, window.location.origin);
}
});
} else {
// No ToC available, use empty headings
noteContext.setContextData("toc", {
headings: [],
activeHeadingId: null,
scrollToHeading: () => {}
});
}
}
if (event.data.type === "pdfjs-viewer-active-heading") {
const currentToc = noteContext.getContextData("toc");
if (currentToc) {
noteContext.setContextData("toc", {
...currentToc,
activeHeadingId: event.data.headingId
});
}
}
if (event.data.type === "pdfjs-viewer-page-info") {
noteContext.setContextData("pdfPages", {
totalPages: event.data.totalPages,
currentPage: event.data.currentPage,
scrollToPage: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-scroll-to-page",
pageNumber: page
}, window.location.origin);
},
requestThumbnail: (page: number) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-thumbnail",
pageNumber: page
}, window.location.origin);
}
});
}
if (event.data.type === "pdfjs-viewer-current-page") {
const currentPages = noteContext.getContextData("pdfPages");
if (currentPages) {
noteContext.setContextData("pdfPages", {
...currentPages,
currentPage: event.data.currentPage
});
}
}
if (event.data.type === "pdfjs-viewer-thumbnail") {
// Forward thumbnail to any listeners
window.dispatchEvent(new CustomEvent("pdf-thumbnail", {
detail: {
pageNumber: event.data.pageNumber,
dataUrl: event.data.dataUrl
}
}));
}
if (event.data.type === "pdfjs-viewer-attachments") {
noteContext.setContextData("pdfAttachments", {
attachments: event.data.attachments,
downloadAttachment: (filename: string) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-download-attachment",
filename
}, window.location.origin);
}
});
}
if (event.data.type === "pdfjs-viewer-layers") {
noteContext.setContextData("pdfLayers", {
layers: event.data.layers,
toggleLayer: (layerId: string, visible: boolean) => {
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-toggle-layer",
layerId,
visible
}, window.location.origin);
}
});
}
}
window.addEventListener("message", handleMessage);
return () => {
window.removeEventListener("message", handleMessage);
};
}, [ note, historyConfig, componentId, blob, noteContext ]);
useTriliumEvent("customDownload", ({ ntxId }) => {
if (ntxId !== noteContext.ntxId) return;
iframeRef.current?.contentWindow?.postMessage({
type: "trilium-request-download"
});
});
return (historyConfig &&
<PdfViewer
iframeRef={iframeRef}
tabIndex={300}
pdfUrl={`../../api/notes/${note.noteId}/open`}
onLoad={() => {
const win = iframeRef.current?.contentWindow;
if (win) {
win.TRILIUM_VIEW_HISTORY_STORE = historyConfig.config;
win.TRILIUM_NOTE_ID = note.noteId;
win.TRILIUM_NTX_ID = noteContext.ntxId;
}
if (iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.addEventListener('click', () => {
appContext.tabManager.activateNoteContext(noteContext.ntxId);
});
}
}}
editable
/>
);
}
interface PdfHeading {
level: number;
text: string;
id: string;
element: null;
}
function convertPdfOutlineToHeadings(outline: PdfOutlineItem[]): PdfHeading[] {
const headings: PdfHeading[] = [];
function flatten(items: PdfOutlineItem[]) {
for (const item of items) {
headings.push({
level: item.level + 1,
text: item.title,
id: item.id,
element: null // PDFs don't have DOM elements
});
if (item.items && item.items.length > 0) {
flatten(item.items);
}
}
}
flatten(outline);
return headings;
}

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