Compare commits

...

141 Commits

Author SHA1 Message Date
Elian Doran
1e95c2da57 chore: add analysis of all issues & PRs 2026-04-10 00:40:21 +03:00
Elian Doran
741ae4b070 chore(server): fix dist creation 2026-04-09 22:31:50 +03:00
Elian Doran
ca13a8accd Update dependency katex to v0.16.45 (#9347) 2026-04-09 18:01:24 +03:00
Elian Doran
78b1f119dc Update dependency dotenv to v17.4.1 (#9346) 2026-04-09 18:00:51 +03:00
Elian Doran
2908b29c0d Translations update from Hosted Weblate (#9351) 2026-04-09 15:25:40 +03:00
Elian Doran
91afa08cdc Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-09 15:24:53 +03:00
Giovi
d93b0442d2 Translated using Weblate (Italian)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2026-04-09 11:10:04 +00:00
Giovi
ce4f9f5f01 Translated using Weblate (Italian)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-04-09 11:10:03 +00:00
Aindriú Mac Giolla Eoin
353d638823 Translated using Weblate (Irish)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-04-09 11:10:01 +00:00
Ali Kaya
995a774140 Translated using Weblate (Turkish)
Currently translated at 7.5% (30 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/tr/
2026-04-09 11:10:00 +00:00
green
c131b245bc Translated using Weblate (Japanese)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-09 11:09:58 +00:00
Ali Kaya
42aabaf9b5 Translated using Weblate (Turkish)
Currently translated at 6.5% (121 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-04-09 11:09:56 +00:00
Bas Wouters
84cce151b8 Translated using Weblate (Dutch)
Currently translated at 4.1% (76 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/nl/
2026-04-09 11:09:54 +00:00
Bas Wouters
e3e6316af7 Translated using Weblate (Dutch)
Currently translated at 26.7% (31 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/nl/
2026-04-09 11:09:53 +00:00
green
96e64c4f17 Translated using Weblate (Japanese)
Currently translated at 100.0% (395 of 395 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-04-09 11:09:52 +00:00
Aindriú Mac Giolla Eoin
3005917256 Translated using Weblate (Irish)
Currently translated at 100.0% (1846 of 1846 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-04-09 11:09:50 +00:00
renovate[bot]
3bf6215249 fix(deps): update dependency katex to v0.16.45 2026-04-09 01:13:57 +00:00
renovate[bot]
2ef045a66d chore(deps): update dependency dotenv to v17.4.1 2026-04-09 01:13:19 +00:00
Elian Doran
743fe5a75d chore(deps): update dependency vite-plugin-static-copy to v4.0.1 (#9333) 2026-04-08 08:03:36 +03:00
renovate[bot]
0c2fdba586 chore(deps): update dependency vite-plugin-static-copy to v4.0.1 2026-04-08 01:34:23 +00:00
Elian Doran
a2c5adec3d Extra bugfixes (#9332) 2026-04-07 21:28:45 +03:00
Elian Doran
6089c8c7c6 Merge branch 'feature/extra_bugfixes' of https://github.com/TriliumNext/Trilium into feature/extra_bugfixes 2026-04-07 20:52:26 +03:00
Elian Doran
f28f725519 fix(server,desktop): not running correctly if placed in dot-hidden directory 2026-04-07 20:46:25 +03:00
Elian Doran
22d853e0b0 Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Elian Doran <contact@eliandoran.me>
2026-04-07 20:26:55 +03:00
Elian Doran
0f1d395651 refactor(script): move runScript to executeScript at BNote level 2026-04-07 19:59:14 +03:00
Elian Doran
3a0bab217d fix(share): hard-coded root-level paths 2026-04-07 19:48:06 +03:00
Elian Doran
f824cb5f15 feat(script): add API to execute backend 2026-04-07 19:40:12 +03:00
Elian Doran
40fd8d6d1a fix(quick_search): ctrl+click & middle click not working (closes #9220) 2026-04-07 19:18:16 +03:00
Elian Doran
e37f73bce0 fix(tab_bar): changing note icon reflect in the tab icon (closes #8994) 2026-04-07 19:11:26 +03:00
Elian Doran
d1cd08972f chore(share): use i18n for more strings 2026-04-07 19:07:20 +03:00
Elian Doran
5a13ca6409 feat(share): render dates on the client side 2026-04-07 19:06:33 +03:00
Elian Doran
eb3fd73415 fix(share): translation not used in template (closes #8722) 2026-04-07 18:54:13 +03:00
Elian Doran
1764fcbba2 fix(script): useContext not provided in imports (closes #9152) 2026-04-07 18:49:54 +03:00
Elian Doran
19f3552bfc fix(calendar): colors unreadable on dark theme (closes #8989)
The calendar event has a light yellow background with light yellow text in dark theme, making it nearly unreadable.

The root cause is a CSS load order issue. The :root defaults in index.css:1-10 are loaded after the dark theme's :root overrides (since component CSS loads after global CSS in Vite), so the defaults (95% lightness, 80% saturation) stomp over the dark theme values (20% lightness, 25% saturation). The background stays light, but --custom-color correctly gets the dark-adjusted (light) text color → light-on-light = bad contrast.

The fix: remove the :root block and use var() fallbacks instead, so there's no :root competition.
2026-04-07 18:48:32 +03:00
Elian Doran
cedce6cf32 feat(relation_map): rename relations through context menu (closes #442) 2026-04-07 18:47:00 +03:00
Elian Doran
26cf215150 fix(share): webviews occupied very little height (closes #9215) 2026-04-07 18:08:23 +03:00
Elian Doran
d21557069c fix(import/zip): ZIPs without language encoding flag importing garbage (closes #3013) 2026-04-07 17:01:34 +03:00
Elian Doran
28b2547229 Translations update from Hosted Weblate (#9327) 2026-04-07 16:13:33 +03:00
Hosted Weblate
d75f556074 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-04-07 15:03:00 +02:00
Ali Kaya
eb66810e59 Translated using Weblate (Turkish)
Currently translated at 5.6% (104 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/tr/
2026-04-07 15:02:55 +02:00
Tomas Adamek
540b39206d Translated using Weblate (Czech)
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2026-04-07 15:02:55 +02:00
Tomas Adamek
5baea04c5d Translated using Weblate (Czech)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2026-04-07 15:02:54 +02:00
Giovi
f5e65748a7 Translated using Weblate (Italian)
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-04-07 15:02:53 +02:00
Elian Doran
de84e09062 Custom dictionary (#9317) 2026-04-07 16:02:34 +03:00
Elian Doran
c81c88c930 fix(log): occassional race condition when creating log dir 2026-04-07 15:54:11 +03:00
Elian Doran
2cb39ea7e3 fix(migration): don't crash on idempotent column creation 2026-04-07 15:30:24 +03:00
Elian Doran
6986963e45 e2e(server): update after changing spellcheck settings 2026-04-07 15:23:19 +03:00
Elian Doran
dc9b0093d9 fix(server): server-side rendered pages use old style 2026-04-07 15:18:38 +03:00
Elian Doran
40f9927842 docs(user): mention updates to spell check 2026-04-07 14:44:51 +03:00
Elian Doran
ff02f5f3ed Merge remote-tracking branch 'origin/main' into feature/custom_dictionary 2026-04-07 14:36:20 +03:00
Elian Doran
22149b94a1 chore(spellcheck): address requested changes 2026-04-07 14:26:55 +03:00
Elian Doran
372d25667f fix(deps): update codemirror themes (#9322) 2026-04-07 14:08:32 +03:00
Elian Doran
21f6cc00eb feat(options/spellcheck): improve display in browser 2026-04-07 13:31:22 +03:00
Elian Doran
620a080128 feat(options/media): hide spellcheck related setting in browser 2026-04-07 13:28:44 +03:00
Elian Doran
6a972aaf3d feat(codemirror): add four more themes 2026-04-07 13:25:25 +03:00
Elian Doran
d878d6b20b chore(deps): update dependency eslint to v10.2.0 (#9324) 2026-04-07 10:57:43 +03:00
Elian Doran
ec075311f4 fix(deps): update dependency preact to v10.29.1 (#9323) 2026-04-07 08:26:20 +03:00
Elian Doran
237c9bb62a fix(deps): update ai sdk (#9321) 2026-04-07 08:22:23 +03:00
Elian Doran
5aa9733bd7 chore(deps): update dependency typedoc-plugin-missing-exports to v4.1.3 (#9320) 2026-04-07 08:20:42 +03:00
renovate[bot]
a157a003c5 chore(deps): update dependency eslint to v10.2.0 2026-04-07 05:20:05 +00:00
renovate[bot]
e40869d3f8 fix(deps): update dependency preact to v10.29.1 2026-04-07 05:19:03 +00:00
Elian Doran
edaecfad4d fix(deps): update univer monorepo to v0.20.0 (#9325) 2026-04-07 08:18:48 +03:00
Elian Doran
983a98ae15 chore(deps): update dependency turndown to v7.2.4 (#9319) 2026-04-07 08:18:10 +03:00
Elian Doran
20ad902feb chore(deps): update dependency @types/node to v24.12.2 (#9318) 2026-04-07 08:15:55 +03:00
renovate[bot]
05de9c6e41 fix(deps): update univer monorepo to v0.20.0 2026-04-07 01:09:37 +00:00
renovate[bot]
df281cbbaa fix(deps): update codemirror themes 2026-04-07 01:07:46 +00:00
renovate[bot]
a979d11b8c fix(deps): update ai sdk 2026-04-07 01:07:09 +00:00
renovate[bot]
f7ff9c114f chore(deps): update dependency typedoc-plugin-missing-exports to v4.1.3 2026-04-07 01:06:33 +00:00
renovate[bot]
807dbdd133 chore(deps): update dependency turndown to v7.2.4 2026-04-07 01:05:58 +00:00
renovate[bot]
4aa944237f chore(deps): update dependency @types/node to v24.12.2 2026-04-07 01:05:24 +00:00
Elian Doran
48db55e3da chore(deps): update dependency vite to v8.0.5 [security] (#9313) 2026-04-06 22:33:44 +03:00
Elian Doran
bd1491e6e5 feat(options/i18n): add reference to spell check 2026-04-06 22:08:23 +03:00
Elian Doran
ac35730e3b feat(options/spellcheck): add button to reload app 2026-04-06 21:56:31 +03:00
Elian Doran
00023adbc0 Revert "feat(options/spellcheck): merge into single card"
This reverts commit 7b056fe1af.
2026-04-06 21:53:17 +03:00
Elian Doran
a70142a4dc feat(options/spellcheck): add button to edit custom words 2026-04-06 21:50:54 +03:00
Elian Doran
7b056fe1af feat(options/spellcheck): merge into single card 2026-04-06 21:44:49 +03:00
Elian Doran
467be38bd1 feat(options/spellcheck): improve language selection 2026-04-06 21:39:58 +03:00
renovate[bot]
933054a095 chore(deps): update dependency vite to v8.0.5 [security] 2026-04-06 18:36:10 +00:00
Elian Doran
f56482157c chore(ai): update system prompt 2026-04-06 21:25:54 +03:00
Elian Doran
5d0c91d91d fix(spellcheck): don't remove local words every time 2026-04-06 20:46:38 +03:00
Elian Doran
03136611a1 fix(spellcheck): don't merge words every time 2026-04-06 20:44:13 +03:00
Elian Doran
3e7488e4f3 feat(spellcheck): clean up local words 2026-04-06 20:36:51 +03:00
Elian Doran
3ed7d48d42 feat(spellcheck): save new words to custom dictionary 2026-04-06 20:28:22 +03:00
Elian Doran
ef72d89172 fix(spellcheck): custom dictionary not actually saved due to CLS 2026-04-06 20:16:02 +03:00
Elian Doran
ad97071862 feat(spellcheck): basic logic to save words 2026-04-06 20:09:29 +03:00
Elian Doran
2291892946 chore(server): create hidden note for the dictionary 2026-04-06 19:55:42 +03:00
Elian Doran
bf8cfa1421 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-06 19:48:42 +03:00
Elian Doran
bdd806efff refactor: delegate theme management completely to client via bootstrap 2026-04-06 19:45:18 +03:00
Elian Doran
c912c4af7b fix(webview): refresh content for SPAs with "query string" in hash (#8883) 2026-04-06 18:50:16 +03:00
Elian Doran
fc7f359f28 Update Web Clipper.md (#9294) 2026-04-06 16:51:46 +03:00
Elian Doran
21598f6189 chore(desktop): change more electron fuses for increased safety 2026-04-06 15:15:47 +03:00
Elian Doran
a1987ea193 Feature/cleanup ck modules (#9310) 2026-04-06 13:27:26 +03:00
Elian Doran
480d167131 fix(desktop): cannot print/export to PDF on Linux wayland (closes #7967) 2026-04-06 13:11:06 +03:00
Elian Doran
d873accf3e Merge remote-tracking branch 'origin/main' into feature/cleanup_ck_modules 2026-04-06 12:41:45 +03:00
Elian Doran
94b448863c chore(deps): update dependency esbuild to v0.28.0 (#9302) 2026-04-06 12:34:15 +03:00
Elian Doran
32acc8555d fix(deps): update dependency @eslint/js to v10 (#9308) 2026-04-06 12:33:56 +03:00
Elian Doran
d68ad84155 Refactor/build warnings due to imports (#9309) 2026-04-06 12:32:32 +03:00
Elian Doran
45e82b7f33 test(ckeditor5-mermaid): fix type errors 2026-04-06 12:31:53 +03:00
Elian Doran
55ad0fe9f0 test(ckeditor5-mermaid): broken tests after change in rendering 2026-04-06 12:30:33 +03:00
Elian Doran
559815273e fix(ckeditor5-mermaid): protect against multiple init 2026-04-06 12:27:12 +03:00
Elian Doran
af76740fd9 fix(ckeditor5-mermaid): protect against stale renders 2026-04-06 12:26:17 +03:00
Elian Doran
7dadd50bfe chore(ckeditor5-mermaid): don't remove parent element on error 2026-04-06 12:25:50 +03:00
Elian Doran
dd4cab22c1 chore(client): address requested changes 2026-04-06 12:22:00 +03:00
Elian Doran
c4d3e776a1 refactor(client): the last circular dependency 2026-04-06 12:20:40 +03:00
Elian Doran
19bb7f5ddb refactor(server): remove unnecessary route 2026-04-06 12:18:36 +03:00
Elian Doran
d212120f9b refactor(client): read locales from common instead of going through the server 2026-04-06 12:16:32 +03:00
Elian Doran
42da1872e7 fix(client): crashing due to circular dependency 2026-04-06 12:10:33 +03:00
Elian Doran
a080b50c45 refactor(client): duplicate toast import 2026-04-06 12:06:27 +03:00
Elian Doran
6d31e9b028 feat(ckeditor5-mermaid): use more modern mechanism for rendering with less flicker 2026-04-06 12:03:29 +03:00
Elian Doran
b606afa858 refactor(ckeditor5-mermaid): get rid of any runtime dependencies 2026-04-06 11:59:24 +03:00
Elian Doran
f9446304b3 refactor(ckeditor5-mermaid): switch to es-toolkit 2026-04-06 11:57:31 +03:00
renovate[bot]
fbe312d580 chore(deps): update dependency esbuild to v0.28.0 2026-04-06 08:54:56 +00:00
Elian Doran
8d383caaff chore(deps): update dependency @electron/fuses to v2 (#9304) 2026-04-06 11:53:07 +03:00
Elian Doran
6caf4fa7ce chore(deps): update dependency copy-webpack-plugin to v14 (#9305) 2026-04-06 11:52:29 +03:00
Elian Doran
606d58b08c chore(ckeditor5-*): remove unnecessary publishing stack 2026-04-06 11:49:47 +03:00
Elian Doran
09258179f0 refactor(client): one more ineffective dynamic import due to appContext 2026-04-06 11:46:35 +03:00
Elian Doran
40e986b188 refactor(client): ineffective dynamic imports 2026-04-06 11:41:35 +03:00
Elian Doran
37e47041bf Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-06 11:41:09 +03:00
Elian Doran
543438bca0 chore(ai): mention instructions for adding new locale 2026-04-06 11:28:10 +03:00
Elian Doran
b31290c1fc fix(deps): update dependency fuse.js to v7.2.0 (#9303) 2026-04-06 11:22:33 +03:00
Elian Doran
d41111a209 docs(dev): remove unnecessary step for adding new locale 2026-04-06 11:21:17 +03:00
Elian Doran
828b523382 feat(i18n): enable Czech 2026-04-06 11:20:58 +03:00
Elian Doran
32409ecbee Translations update from Hosted Weblate (#9295) 2026-04-06 11:16:03 +03:00
renovate[bot]
3ca2cec63a chore(deps): update dependency copy-webpack-plugin to v14 2026-04-06 08:14:46 +00:00
Francis C.
1ed2db0c82 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-04-06 08:13:48 +00:00
Francis C.
2423b74dd0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2026-04-06 08:13:48 +00:00
Tomas Adamek
3f781ea298 Translated using Weblate (Czech)
Currently translated at 98.9% (1822 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
30c5c49aef Translated using Weblate (Czech)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
9421e39c34 Translated using Weblate (Czech)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/cs/
2026-04-06 08:13:46 +00:00
Tomas Adamek
c46805cf4f Translated using Weblate (Czech)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2026-04-06 08:13:46 +00:00
Elian Doran
f181343fca chore(deps): update dependency @redocly/cli to v2.25.4 (#9297) 2026-04-06 11:13:37 +03:00
Elian Doran
8a512e4f73 chore(deps): update dependency electron to v41 (#9306) 2026-04-06 11:13:01 +03:00
Elian Doran
06a3750168 chore(renovate): group AI SDK updates 2026-04-06 11:09:07 +03:00
renovate[bot]
35c1a5642d fix(deps): update dependency @eslint/js to v10 2026-04-06 01:05:51 +00:00
renovate[bot]
f29df2ad28 chore(deps): update dependency electron to v41 2026-04-06 01:03:55 +00:00
renovate[bot]
75a5714451 chore(deps): update dependency @electron/fuses to v2 2026-04-06 01:01:53 +00:00
renovate[bot]
2882863b5b fix(deps): update dependency fuse.js to v7.2.0 2026-04-06 01:00:57 +00:00
renovate[bot]
773b6cca14 chore(deps): update dependency @redocly/cli to v2.25.4 2026-04-06 00:54:32 +00:00
ce603
15505ffcd8 Update docs/User Guide/User Guide/Installation & Setup/Web Clipper.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-05 14:41:25 -04:00
ce603
96cef35f09 Update Web Clipper.md
Update web clipper docs with published store links
2026-04-05 14:28:51 -04:00
contributor
ac24c69858 fix(webview): refresh content for SPAs with "query string" in hash 2026-03-02 23:35:53 +02:00
233 changed files with 11993 additions and 3604 deletions

View File

@@ -320,6 +320,7 @@ Trilium provides powerful user scripting capabilities:
- Use translation system via `t()` function
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
## Testing Conventions

View File

@@ -121,6 +121,13 @@ Trilium provides powerful user scripting capabilities:
- **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json`
### Electron Desktop App
- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts`
- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side
- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server)
### Security Considerations
- Per-note encryption with granular protected sessions
@@ -152,6 +159,20 @@ Trilium provides powerful user scripting capabilities:
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
### Adding Hidden System Notes
The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`.
1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts`
2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"`
3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure
4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content`
5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes
6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts`
### Writing to Notes from Server Services
- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`)
- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
### Adding New LLM Tools
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.

View File

@@ -16,11 +16,11 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@redocly/cli": "2.25.3",
"@redocly/cli": "2.25.4",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"typedoc": "0.28.18",
"typedoc-plugin-missing-exports": "4.1.2"
"typedoc-plugin-missing-exports": "4.1.3"
}
}

View File

@@ -34,14 +34,14 @@
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@univerjs/preset-sheets-conditional-formatting": "0.19.0",
"@univerjs/preset-sheets-core": "0.19.0",
"@univerjs/preset-sheets-data-validation": "0.19.0",
"@univerjs/preset-sheets-filter": "0.19.0",
"@univerjs/preset-sheets-find-replace": "0.19.0",
"@univerjs/preset-sheets-note": "0.19.0",
"@univerjs/preset-sheets-sort": "0.19.0",
"@univerjs/presets": "0.19.0",
"@univerjs/preset-sheets-conditional-formatting": "0.20.0",
"@univerjs/preset-sheets-core": "0.20.0",
"@univerjs/preset-sheets-data-validation": "0.20.0",
"@univerjs/preset-sheets-filter": "0.20.0",
"@univerjs/preset-sheets-find-replace": "0.20.0",
"@univerjs/preset-sheets-note": "0.20.0",
"@univerjs/preset-sheets-sort": "0.20.0",
"@univerjs/presets": "0.20.0",
"@zumer/snapdom": "2.7.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
@@ -57,7 +57,7 @@
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.44",
"katex": "0.16.45",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
@@ -65,7 +65,7 @@
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"panzoom": "9.4.4",
"preact": "10.29.0",
"preact": "10.29.1",
"react-i18next": "17.0.2",
"react-window": "2.2.7",
"reveal.js": "6.0.0",
@@ -87,6 +87,6 @@
"happy-dom": "20.8.9",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "4.0.0"
"vite-plugin-static-copy": "4.0.1"
}
}

View File

@@ -1,10 +1,11 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { SqlExecuteResponse } from "@triliumnext/commons";
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
import type { NativeImage, TouchBar } from "electron";
import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
@@ -563,7 +564,7 @@ export class AppContext extends Component {
*/
async earlyInit() {
await options.initializedPromise;
await initLocale();
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
}
setLayout(layout: Layout) {
@@ -578,7 +579,6 @@ export class AppContext extends Component {
this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000);
}

View File

@@ -54,7 +54,7 @@ function initOnElectron() {
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
initDarkOrLightMode(style);
initDarkOrLightMode();
initTransparencyEffects(style, currentWindow);
initFullScreenDetection(currentWindow);
@@ -119,11 +119,11 @@ function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Elec
*
* @param style the root CSS element to read variables from.
*/
function initDarkOrLightMode(style: CSSStyleDeclaration) {
function initDarkOrLightMode() {
let themeSource: typeof nativeTheme.themeSource = "system";
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
const themeStyle = window.glob.getThemeStyle();
if (themeStyle !== "auto") {
themeSource = themeStyle;
}

View File

@@ -1,5 +1,6 @@
import { getNoteIcon } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
@@ -1014,7 +1015,6 @@ export default class FNote {
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
return await bundleService.getAndExecuteBundle(this.noteId);
} else if (env === "backend") {
await server.post(`script/run/${this.noteId}`);

View File

@@ -1,3 +1,5 @@
import { getThemeStyle } from "./services/theme";
async function bootstrap() {
showSplash();
await setupGlob();
@@ -38,6 +40,7 @@ async function setupGlob() {
...json,
activeDialog: null
};
window.glob.getThemeStyle = getThemeStyle;
}
async function loadBootstrapCss() {
@@ -49,31 +52,65 @@ async function loadBootstrapCss() {
}
}
function loadStylesheets() {
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
type StylesheetRef = {
href: string;
media?: string;
};
const cssToLoad: string[] = [];
if (device !== "print") {
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`);
function getConfiguredThemeStylesheets(stylesheetsPath: string, theme: string, customThemeCssUrl?: string) {
if (theme === "auto") {
return [{ href: `${stylesheetsPath}/theme-dark.css`, media: "(prefers-color-scheme: dark)" }];
}
for (const href of cssToLoad) {
if (theme === "dark") {
return [{ href: `${stylesheetsPath}/theme-dark.css` }];
}
if (theme === "next") {
return [
{ href: `${stylesheetsPath}/theme-next-light.css` },
{ href: `${stylesheetsPath}/theme-next-dark.css`, media: "(prefers-color-scheme: dark)" }
];
}
if (theme === "next-light") {
return [{ href: `${stylesheetsPath}/theme-next-light.css` }];
}
if (theme === "next-dark") {
return [{ href: `${stylesheetsPath}/theme-next-dark.css` }];
}
if (theme !== "light" && customThemeCssUrl) {
return [{ href: customThemeCssUrl }];
}
return [];
}
function loadStylesheets() {
const { device, assetPath, theme, themeBase, customThemeCssUrl } = window.glob;
const stylesheetsPath = `${assetPath}/stylesheets`;
const cssToLoad: StylesheetRef[] = [];
if (device !== "print") {
cssToLoad.push({ href: `${stylesheetsPath}/ckeditor-theme.css` });
cssToLoad.push({ href: `api/fonts` });
cssToLoad.push({ href: `${stylesheetsPath}/theme-light.css` });
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, theme, customThemeCssUrl));
if (themeBase) {
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, themeBase));
}
cssToLoad.push({ href: `${stylesheetsPath}/style.css` });
}
for (const { href, media } of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
if (media) {
linkEl.media = media;
}
document.head.appendChild(linkEl);
}
}

View File

@@ -1,12 +1,14 @@
import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import type { BrowserWindow } from "electron";
import type { CommandNames, AppContext } from "../components/app_context.js";
import type { CommandNames } from "../components/app_context.js";
import appContext from "../components/app_context.js";
import zoomService from "../components/zoom.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import { t } from "../services/i18n.js";
import options from "../services/options.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
function setupContextMenu() {
const electron = utils.dynamicRequire("electron");
@@ -15,8 +17,6 @@ function setupContextMenu() {
// FIXME: Remove typecast once Electron is properly integrated.
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
let appContext: AppContext;
webContents.on("context-menu", (event, params) => {
const { editFlags } = params;
const hasText = params.selectionText.trim().length > 0;
@@ -38,7 +38,7 @@ function setupContextMenu() {
items.push({
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
uiIcon: "bx bx-plus",
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
});
items.push({ kind: "separator" });
@@ -141,7 +141,7 @@ function setupContextMenu() {
}
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" });
@@ -155,10 +155,6 @@ function setupContextMenu() {
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
uiIcon: "bx bx-search",
handler: async () => {
if (!appContext) {
appContext = (await import("../components/app_context.js")).default;
}
await appContext.triggerCommand("searchNotes", {
searchString: params.selectionText
});

View File

@@ -4,6 +4,7 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import FNote from "./entities/fnote";
import content_renderer from "./services/content_renderer";
import { applyInlineMermaid } from "./services/content_renderer_text";
import froca from "./services/froca";
import { dynamicRequire, isElectron } from "./services/utils";
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
@@ -30,7 +31,6 @@ async function main() {
if (!noteId) return;
await import("./print.css");
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
const bodyWrapper = document.createElement("div");

View File

@@ -26,7 +26,7 @@ type WithNoteId<T> = T & {
};
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
script,
params

View File

@@ -1,3 +1,6 @@
import { t } from "./i18n.js";
import toast from "./toast.js";
export function copyText(text: string) {
if (!text) {
return;
@@ -6,29 +9,26 @@ export function copyText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
}
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
} catch (e) {
console.warn(e);
return false;
}
}
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
export function copyTextWithToast(text: string) {
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {

View File

@@ -1,9 +1,11 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import keyboardActionsService from "./keyboard_actions.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;

View File

@@ -1,14 +1,16 @@
import LoadResults from "./load_results.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import type { OptionNames } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js";
import type { OptionNames } from "@triliumnext/commons";
import froca from "./froca.js";
import LoadResults from "./load_results.js";
import noteAttributeCache from "./note_attribute_cache.js";
import options from "./options.js";
import utils from "./utils.js";
async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges);
@@ -63,7 +65,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
missingNoteIds.push((entity as FBranchRow).parentNoteId);
} else if (entityName === "attributes") {
let attributeEntity = entity as FAttributeRow;
const attributeEntity = entity as FAttributeRow;
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
missingNoteIds.push(attributeEntity.value);
}
@@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate();
}
const appContext = (await import("../components/app_context.js")).default;
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}

View File

@@ -1,4 +1,4 @@
import { Fragment, h, VNode } from "preact";
import { createContext, Fragment, h, VNode } from "preact";
import * as hooks from "preact/hooks";
import ActionButton from "../widgets/react/ActionButton";
@@ -47,6 +47,7 @@ export const preactAPI = Object.freeze({
// Core
h,
Fragment,
createContext,
/**
* Method that must be run for widget scripts that run on Preact, using JSX. The method just returns the same definition, reserved for future typechecking and perhaps validation purposes.

View File

@@ -1,21 +1,14 @@
import options from "./options.js";
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export let translationsInitializedPromise = $.Deferred();
export const translationsInitializedPromise = $.Deferred();
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales");
export async function initLocale(locale: LOCALE_IDS = "en") {
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
@@ -32,11 +25,7 @@ export async function initLocale() {
}
export function getAvailableLocales() {
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
}
return locales;
return LOCALES;
}
/**
@@ -47,7 +36,7 @@ export function getAvailableLocales() {
*/
export function getLocaleById(localeId: string | null | undefined) {
if (!localeId) return null;
return locales?.find((l) => l.id === localeId) ?? null;
return LOCALES.find((l) => l.id === localeId) ?? null;
}
export const t = i18next.t;

View File

@@ -1,3 +1,4 @@
import { t } from "./i18n.js";
import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js";
@@ -32,8 +33,7 @@ async function getHeaders(headers?: Headers) {
return {};
}
const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
// headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default
@@ -344,6 +344,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
} catch (e) {}
}
// Dynamic import to avoid circular dependency (toast → app_context → options → server).
const toastService = (await import("./toast.js")).default;
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
@@ -357,7 +358,6 @@ async function reportError(method: string, url: string, statusCode: number, resp
...response
});
} else {
const { t } = await import("./i18n.js");
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
toastService.showPersistent({
id: "trafik-blocked",
@@ -371,8 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
const { logError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`);
window.logError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -0,0 +1,35 @@
export function getThemeStyle(): "auto" | "light" | "dark" {
const configuredTheme = window.glob?.theme;
if (configuredTheme === "auto" || configuredTheme === "next") {
return "auto";
}
if (configuredTheme === "light" || configuredTheme === "dark") {
return configuredTheme;
}
if (configuredTheme === "next-light") {
return "light";
}
if (configuredTheme === "next-dark") {
return "dark";
}
const style = window.getComputedStyle(document.body);
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
return themeStyle as "light" | "dark";
}
return "auto";
}
export function getEffectiveThemeStyle(): "light" | "dark" {
const themeStyle = getThemeStyle();
if (themeStyle === "auto") {
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
return themeStyle === "dark" ? "dark" : "light";
}

View File

@@ -455,9 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
const activeContext = glob.appContext?.tabManager?.getActiveContext();
if (!activeContext) {
return;
}
@@ -467,7 +465,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
glob.appContext?.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,

View File

@@ -1,13 +1,15 @@
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type { EntityChange } from "../server_types.js";
import bundleService from "./bundle.js";
import froca from "./froca.js";
import frocaUpdater from "./froca_updater.js";
import { t } from "./i18n.js";
import options from "./options.js";
import server from "./server.js";
import toast from "./toast.js";
import utils from "./utils.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
@@ -126,20 +128,14 @@ async function handleMessage(event: MessageEvent<any>) {
} else if (message.type === "frontend-update") {
await executeFrontendUpdate(message.data.entityChanges);
} else if (message.type === "sync-hash-check-failed") {
toastService.showError(t("ws.sync-check-failed"), 60000);
toast.showError(t("ws.sync-check-failed"), 60000);
} else if (message.type === "consistency-checks-failed") {
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
toast.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (message.type === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages });
} else if (message.type === "toast") {
toastService.showMessage(message.message);
toast.showMessage(message.message);
} else if (message.type === "execute-script") {
// TODO: Remove after porting the file
// @ts-ignore
const bundleService = (await import("./bundle.js")).default as any;
// TODO: Remove after porting the file
// @ts-ignore
const froca = (await import("./froca.js")).default as any;
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
@@ -161,7 +157,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
return new Promise<void>((res, rej) => {
entityChangeIdReachedListeners.push({
desiredEntityChangeId: desiredEntityChangeId,
desiredEntityChangeId,
resolvePromise: res,
start: Date.now()
});
@@ -205,7 +201,7 @@ async function consumeFrontendUpdateData() {
} else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toastService.showError(t("ws.encountered-error", { message: e.message }));
toast.showError(t("ws.encountered-error", { message: e.message }));
}
}

View File

@@ -1,11 +0,0 @@
/* Import the light color scheme.
* This is the base color scheme, always active and overridden by the dark
* color scheme stylesheet when necessary. */
@import url(./theme-next-light.css);
/* Import the dark color scheme when the system preference is set to dark mode */
@import url(./theme-next-dark.css) (prefers-color-scheme: dark);
:root {
--theme-style-auto: true;
}

View File

@@ -1,11 +0,0 @@
/* Import the light color scheme.
* This is the base color scheme, always active and overridden by the dark
* color scheme stylesheet when necessary. */
@import url(./theme-light.css);
/* Import the dark color scheme when the system preference is set to dark mode */
@import url(./theme-dark.css) (prefers-color-scheme: dark);
:root {
--theme-style-auto: true;
}

View File

@@ -1129,9 +1129,7 @@
"spellcheck": {
"title": "التدقيق الاملائي",
"enable": "تفعيل التدقيق الاملائي",
"language_code_label": "رمز اللغة او رموز اللغات",
"available_language_codes_label": "رموز اللغات المتاحة:",
"language_code_placeholder": "على سبيل المثال \"en-US\", \"de-AI\""
"language_code_label": "رمز اللغة او رموز اللغات"
},
"note-map": {
"button-link-map": "خريطة الروابط",

View File

@@ -1437,9 +1437,6 @@
"description": "这些选项仅适用于桌面版本,浏览器将使用其原生的拼写检查功能。",
"enable": "启用拼写检查",
"language_code_label": "语言代码",
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
"multiple_languages_info": "多种语言可以用逗号分隔,例如 \"en-US, de-DE, cs\"。 ",
"available_language_codes_label": "可用的语言代码:",
"restart-required": "拼写检查选项的更改将在应用重启后生效。"
},
"sync_2": {

File diff suppressed because it is too large Load Diff

View File

@@ -1386,9 +1386,6 @@
"description": "Diese Optionen gelten nur für Desktop-Builds. Browser verwenden ihre eigene native Rechtschreibprüfung.",
"enable": "Aktiviere die Rechtschreibprüfung",
"language_code_label": "Sprachcode(s)",
"language_code_placeholder": "zum Beispiel \"en-US\", \"de-AT\"",
"multiple_languages_info": "Mehrere Sprachen können mit einem Komma getrennt werden z.B. \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Verfügbare Sprachcodes:",
"restart-required": "Änderungen an den Rechtschreibprüfungsoptionen werden nach dem Neustart der Anwendung wirksam."
},
"sync_2": {

View File

@@ -1074,6 +1074,7 @@
"edit_title": "Edit title",
"rename_note": "Rename note",
"enter_new_title": "Enter new note title:",
"rename_relation": "Rename relation",
"remove_relation": "Remove relation",
"confirm_remove_relation": "Are you sure you want to remove the relation?",
"specify_new_relation_name": "Specify new relation name (allowed characters: alphanumeric, colon and underscore):",
@@ -1498,12 +1499,15 @@
"spellcheck": {
"title": "Spell Check",
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
"enable": "Enable spellcheck",
"language_code_label": "Language code(s)",
"language_code_placeholder": "for example \"en-US\", \"de-AT\"",
"multiple_languages_info": "Multiple languages can be separated by comma, e.g. \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Available language codes:",
"restart-required": "Changes to the spell check options will take effect after application restart."
"enable": "Check spelling",
"language_code_label": "Spell Check Languages",
"restart-required": "Changes to the spell check options will take effect after application restart.",
"custom_dictionary_title": "Custom Dictionary",
"custom_dictionary_description": "Words added to the dictionary are synced across all your devices.",
"custom_dictionary_edit": "Custom words",
"custom_dictionary_edit_description": "Edit the list of words that should not be flagged by the spell checker. Changes will be visible after a restart.",
"custom_dictionary_open": "Edit dictionary",
"related_description": "Configure spell check languages and custom dictionary."
},
"sync_2": {
"config_title": "Sync Configuration",

View File

@@ -1432,9 +1432,6 @@
"description": "Estas opciones se aplican sólo para compilaciones de escritorio; los navegadores utilizarán su corrector ortográfico nativo.",
"enable": "Habilitar corrector ortográfico",
"language_code_label": "Código(s) de idioma",
"language_code_placeholder": "por ejemplo \"en-US\", \"de-AT\"",
"multiple_languages_info": "Múltiples idiomas se pueden separar por coma, por ejemplo \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Códigos de idioma disponibles:",
"restart-required": "Los cambios en las opciones de corrección ortográfica entrarán en vigor después del reinicio de la aplicación."
},
"sync_2": {

View File

@@ -1391,9 +1391,6 @@
"description": "Ces options s'appliquent uniquement aux versions de bureau, les navigateurs utiliseront leur propre vérification orthographique native.",
"enable": "Activer la vérification orthographique",
"language_code_label": "Code(s) de langue",
"language_code_placeholder": "par exemple \"fr-FR\", \"en-US\", \"de-AT\"",
"multiple_languages_info": "Plusieurs langues peuvent être séparées par une virgule, par ex. \"fr-FR, en-US, de-DE, cs\". ",
"available_language_codes_label": "Codes de langue disponibles :",
"restart-required": "Les modifications apportées aux options de vérification orthographique prendront effet après le redémarrage de l'application."
},
"sync_2": {

View File

@@ -1071,7 +1071,8 @@
"note_already_in_diagram": "Tabhair faoi deara go bhfuil \"{{title}}\" sa léaráid cheana féin.",
"enter_title_of_new_note": "Cuir isteach teideal an nóta nua",
"default_new_note_title": "nóta nua",
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur"
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur",
"rename_relation": "Athainmnigh an gaol"
},
"backend_log": {
"refresh": "Athnuachan"
@@ -1468,12 +1469,15 @@
"spellcheck": {
"title": "Seiceáil Litrithe",
"description": "Ní bhaineann na roghanna seo ach le leaganacha deisce, úsáidfidh brabhsálaithe a seiceáil litrithe dúchasach féin.",
"enable": "Cumasaigh seiceáil litrithe",
"language_code_label": "Cód(anna) teanga",
"language_code_placeholder": "mar shampla \"en-US\", \"de-AT\"",
"multiple_languages_info": "Is féidir camóg a úsáid chun teangacha iolracha a dheighilt óna chéile, m.sh. \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Cóid teanga atá ar fáil:",
"restart-required": "Tiocfaidh athruithe ar na roghanna seiceála litrithe i bhfeidhm tar éis atosú an fheidhmchláir."
"enable": "Seiceáil litriú",
"language_code_label": "Seiceáil Litrithe Teangacha",
"restart-required": "Tiocfaidh athruithe ar na roghanna seiceála litrithe i bhfeidhm tar éis atosú an fheidhmchláir.",
"custom_dictionary_title": "Foclóir Saincheaptha",
"custom_dictionary_description": "Déantar focail a chuirtear leis an bhfoclóir a sioncrónú ar fud do ghléasanna go léir.",
"custom_dictionary_edit": "Focail saincheaptha",
"custom_dictionary_edit_description": "Cuir an liosta focal in eagar nach ceart don seiceálaí litrithe a mharcáil. Beidh athruithe le feiceáil tar éis atosaithe.",
"custom_dictionary_open": "Cuir an foclóir in eagar",
"related_description": "Cumraigh teangacha seiceála litrithe agus foclóir saincheaptha."
},
"sync_2": {
"config_title": "Cumraíocht Sioncrónaithe",
@@ -2294,7 +2298,9 @@
"sample_user_journey": "Turas Úsáideora",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
"sample_ishikawa": "Ishikawa",
"sample_treeview": "Radharc Crann",
"sample_wardley": "Léarscáil Wardley"
},
"llm_chat": {
"placeholder": "Clóscríobh teachtaireacht...",
@@ -2404,6 +2410,9 @@
"processing": "Ag próiseáil...",
"processing_started": "Tá próiseáil OCR tosaithe. Fan nóiméad agus athnuachan le do thoil.",
"processing_failed": "Theip ar phróiseáil OCR a thosú",
"view_extracted_text": "Féach ar théacs eastósctha (OCR)"
"view_extracted_text": "Féach ar théacs eastósctha (OCR)",
"processing_complete": "Próiseáil OCR críochnaithe.",
"text_filtered_low_confidence": "Bhraith OCR téacs le muinín {{confidence}}%, ach caitheadh leis é mar is é {{threshold}}% an tairseach íosta atá agat.",
"open_media_settings": "Oscail Socruithe"
}
}

View File

@@ -1461,9 +1461,6 @@
"description": "ये विकल्प सिर्फ़ डेस्कटॉप वर्जन के लिए हैं, ब्राउज़र अपना स्पेल चेक इस्तेमाल करेंगे।",
"enable": "स्पेल चेक चालू करें",
"language_code_label": "भाषा कोड (Language code)",
"language_code_placeholder": "जैसे \"en-US\", \"hi-IN\"",
"multiple_languages_info": "कई भाषाओं को कॉमा से अलग किया जा सकता है, जैसे \"en-US, hi-IN\"। ",
"available_language_codes_label": "उपलब्ध भाषा कोड:",
"restart-required": "स्पेल चेक में बदलाव ऐप रीस्टार्ट करने के बाद ही दिखेंगे।"
},
"sync_2": {

View File

@@ -538,12 +538,12 @@
"new_tab": "Nuova scheda"
},
"toc": {
"table_of_contents": "Sommario",
"table_of_contents": "Tabella dei Contenuti",
"options": "Opzioni",
"no_headings": "Nessun titolo."
},
"table_of_contents": {
"title": "Sommario",
"title": "Tabella dei Contenuti",
"description": "L'indice apparirà nelle note di testo quando la nota contiene più di un numero definito di titoli. È possibile personalizzare questo numero:",
"unit": "titoli",
"disable_info": "È anche possibile utilizzare questa opzione per disattivare efficacemente l'indice impostando un numero molto alto.",
@@ -593,7 +593,7 @@
"collapseExpand": "collassa/espande il nodo",
"notSet": "non impostato",
"goBackForwards": "indietro/avanti nella cronologia",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">finestra \"Vai a\"</a>",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Vai a\"</a>",
"title": "Scheda riassuntiva",
"noteNavigation": "Nota navigazione",
"scrollToActiveNote": "scorri fino alla nota attiva",
@@ -853,7 +853,7 @@
"archived": "Le note con questa etichetta non saranno visibili per impostazione predefinita nei risultati di ricerca (anche nelle finestre di dialogo Vai a, Aggiungi collegamento ecc.).",
"run_on_instance": "Definire su quale istanza di Trilium eseguire questa operazione. L'impostazione predefinita è tutte le istanze.",
"exclude_from_export": "le note (con la loro sottostruttura) non saranno incluse in nessuna esportazione di note",
"run": "definisce su quali eventi deve essere eseguito lo script. I valori possibili sono:\n<ul>\n<li>frontendStartup - quando il frontend Trilium viene avviato (o aggiornato), ma non su dispositivi mobili.</li>\n<li>mobileStartup - quando il frontend Trilium viene avviato (o aggiornato) su dispositivi mobili.</li>\n<li>backendStartup - quando viene avviato il backend Trilium</li>\n<li>hourly - eseguire una volta all'ora. È possibile utilizzare l'etichetta aggiuntiva <code>runAtHour</code> per specificare a che ora.</li>\n<li>daily - eseguire una volta al giorno</li>\n</ul>",
"run": "definisce su quali eventi deve essere eseguito lo script. I valori possibili sono:\n<ul>\n<li>frontendStartup - quando il frontend Trilium viene avviato (o aggiornato), ma non su dispositivi mobili.</li>\n<li>mobileStartup - quando il frontend Trilium viene avviato (o aggiornato) su dispositivi mobili.</li>\n<li>backendStartup - quando viene avviato il backend Trilium.</li>\n<li>hourly - eseguire una volta all'ora. È possibile utilizzare l'etichetta aggiuntiva <code>runAtHour</code> per specificare a che ora.</li>\n<li>daily - eseguire una volta al giorno.</li>\n</ul>",
"run_at_hour": "A che ora deve essere eseguito. Deve essere utilizzato insieme a <code>#run=hourly</code>. Può essere definito più volte per più esecuzioni durante il giorno.",
"disable_inclusion": "gli script con questa etichetta non saranno inclusi nell'esecuzione dello script principale.",
"sorted": "mantiene le note figlie ordinate alfabeticamente per titolo",
@@ -1100,7 +1100,7 @@
"show_help": "Mostra aiuto",
"about": "Informazioni su Trilium Notes",
"logout": "Esci",
"show-cheatsheet": "Mostra il foglietto illustrativo",
"show-cheatsheet": "Mostra la scheda riassuntiva",
"toggle-zen-mode": "Modalità Zen",
"new-version-available": "Nuovo aggiornamento disponibile",
"download-update": "Ottieni la versione {{latestVersion}}",
@@ -1149,7 +1149,8 @@
"export_as_image": "Esporta come immagine",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)",
"note_map": "Mappa"
"note_map": "Mappa",
"view_ocr_text": "Visualizza il testo OCR"
},
"onclick_button": {
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
@@ -1439,7 +1440,8 @@
"note_already_in_diagram": "Nota che \"{{title}}\" è già presente nel diagramma.",
"enter_title_of_new_note": "Inserisci il titolo della nuova nota",
"default_new_note_title": "nuova nota",
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota"
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota",
"rename_relation": "Rinomina relazione"
},
"vacuum_database": {
"title": "Pulizia del database",
@@ -1541,12 +1543,28 @@
},
"images": {
"images_section_title": "Immagini",
"download_images_automatically": "Scarica automaticamente le immagini per l'utilizzo offline.",
"download_images_description": "L'HTML incollato può contenere riferimenti a immagini online; Trilium troverà tali riferimenti e scaricherà le immagini in modo che siano disponibili offline.",
"enable_image_compression": "Abilita la compressione delle immagini",
"max_image_dimensions": "Larghezza/altezza massima di un'immagine (l'immagine verrà ridimensionata se supera questa impostazione).",
"download_images_automatically": "Scarica automaticamente le immagini",
"download_images_description": "Scarica le immagini online a cui si fa riferimento nel codice HTML incollato, in modo che siano disponibili offline.",
"enable_image_compression": "Compressione delle immagini",
"max_image_dimensions": "Dimensioni massime dell'immagine",
"max_image_dimensions_unit": "pixel",
"jpeg_quality_description": "Qualità JPEG (10 - qualità peggiore, 100 - qualità migliore, 50 - 85 è consigliato)"
"jpeg_quality_description": "Il range consigliato è compreso tra 50 e 85. Valori più bassi riducono le dimensioni del file, mentre valori più alti preservano i dettagli.",
"enable_image_compression_description": "Comprimi e ridimensiona le immagini al momento del caricamento o dell'inserimento.",
"max_image_dimensions_description": "Le immagini che superano queste dimensioni verranno ridimensionate automaticamente.",
"jpeg_quality": "Qualità JPEG",
"ocr_section_title": "Estrazione di testo (OCR)",
"ocr_related_content_languages": "Lingue dei contenuti (utilizzate per l'estrazione del testo)",
"ocr_auto_process": "Elaborazione automatica dei nuovi file",
"ocr_auto_process_description": "Estrai automaticamente il testo dai file appena caricati o incollati.",
"ocr_min_confidence": "Livello minimo di confidenza",
"ocr_confidence_description": "Estrai solo il testo che supera questa soglia di affidabilità. Valori inferiori includono più testo, ma potrebbero risultare meno accurati.",
"batch_ocr_title": "Elabora i file esistenti",
"batch_ocr_description": "Estrai il testo da tutte le immagini, i PDF e i documenti Office presenti nei tuoi appunti. L'operazione potrebbe richiedere un po' di tempo a seconda del numero di file.",
"batch_ocr_start": "Avvia l'elaborazione in batch",
"batch_ocr_starting": "Avvio dell'elaborazione in batch...",
"batch_ocr_progress": "Elaborazione di {{processed}} su {{total}} file...",
"batch_ocr_completed": "Elaborazione in batch completata! Sono stati elaborati {{processed}} file.",
"batch_ocr_error": "Errore durante l'elaborazione in batch: {{error}}"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Timeout cancellazione allegato",
@@ -1654,12 +1672,15 @@
"spellcheck": {
"title": "Controllo ortografico",
"description": "Queste opzioni sono valide solo per le versioni desktop; i browser utilizzeranno il proprio controllo ortografico nativo.",
"enable": "Abilita il controllo ortografico",
"language_code_label": "Codice/i della lingua",
"language_code_placeholder": "ad esempio \"en-US\", \"de-AT\"",
"multiple_languages_info": "È possibile separare più lingue con una virgola, ad esempio \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Codici lingua disponibili:",
"restart-required": "Le modifiche alle opzioni di controllo ortografico avranno effetto dopo il riavvio dell'applicazione."
"enable": "Controlla l'ortografia",
"language_code_label": "Lingue del controllo ortografico",
"restart-required": "Le modifiche alle opzioni di controllo ortografico avranno effetto dopo il riavvio dell'applicazione.",
"custom_dictionary_title": "Dizionario personalizzato",
"custom_dictionary_description": "Le parole aggiunte al dizionario vengono sincronizzate su tutti i tuoi dispositivi.",
"custom_dictionary_edit": "Parole personalizzate",
"custom_dictionary_edit_description": "Modifica l'elenco delle parole che non devono essere segnalate dal correttore ortografico. Le modifiche saranno visibili dopo il riavvio.",
"custom_dictionary_open": "Modifica il dizionario",
"related_description": "Configura le lingue del controllo ortografico e il dizionario personalizzato."
},
"api_log": {
"close": "Vicino"
@@ -1940,7 +1961,7 @@
},
"content_language": {
"title": "Lingue dei contenuti",
"description": "Seleziona una o più lingue che desideri visualizzare nella sezione \"Proprietà di base\" di una nota di testo di sola lettura o modificabile. Ciò consentirà funzionalità come il controllo ortografico o il supporto per la scrittura da destra a sinistra."
"description": "Seleziona una o più lingue che devono comparire nell'elenco di selezione delle lingue nella sezione \"Proprietà di base\" di una nota di testo in sola lettura o modificabile. Ciò consentirà di utilizzare funzioni quali il controllo ortografico, il supporto per la scrittura da destra a sinistra e l'estrazione del testo (OCR)."
},
"switch_layout_button": {
"title_vertical": "Sposta il riquadro di modifica in basso",
@@ -2247,7 +2268,9 @@
"sample_user_journey": "Percorso dell'utente",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
"sample_ishikawa": "Ishikawa",
"sample_treeview": "TreeView",
"sample_wardley": "Mappa di Wardley"
},
"llm_chat": {
"placeholder": "Scrivi un messaggio...",
@@ -2278,7 +2301,8 @@
"note_context_enabled": "Clicca qui per disattivare il contesto della nota: {{title}}",
"note_context_disabled": "Clicca per includere la nota corrente nel contesto",
"no_provider_message": "Non è stato configurato alcun fornitore di IA. Aggiungine uno per iniziare a chattare.",
"add_provider": "Aggiungi un fornitore di IA"
"add_provider": "Aggiungi un fornitore di IA",
"sources_summary": "{{count}} fonti provenienti da {{sites}} siti"
},
"sidebar_chat": {
"title": "Chat AI",
@@ -2304,6 +2328,61 @@
"delete_provider_confirmation": "Sei sicuro di voler eliminare il provider \"{{name}}\"?",
"api_key": "Chiave API",
"api_key_placeholder": "Inserisci la tua chiave API",
"cancel": "Annulla"
"cancel": "Annulla",
"feature_not_enabled": "Abilita la funzione sperimentale LLM in Impostazioni → Avanzate → Funzioni sperimentali per utilizzare le integrazioni basate sull'intelligenza artificiale.",
"mcp_title": "MCP (Model Context Protocol)",
"mcp_enabled": "Server MCP",
"mcp_enabled_description": "Rendi pubblico un endpoint MCP (Model Context Protocol) in modo che gli assistenti di programmazione basati sull'intelligenza artificiale (ad esempio Claude Code, GitHub Copilot) possano leggere e modificare le tue note. L'endpoint è accessibile solo da localhost.",
"mcp_endpoint_title": "URL dell'endpoint",
"mcp_endpoint_description": "Aggiungi questo URL alla configurazione MCP del tuo assistente AI",
"tools": {
"search_notes": "Cerca nelle note",
"get_note": "Prendi nota",
"get_note_content": "Visualizza il contenuto della nota",
"update_note_content": "Aggiorna il contenuto della nota",
"append_to_note": "Aggiungi alla nota",
"create_note": "Crea nota",
"get_attributes": "Recupera gli attributi",
"get_attribute": "Ottieni attributo",
"set_attribute": "Imposta attributo",
"delete_attribute": "Elimina attributo",
"get_child_notes": "Recupera le note relative ai figli",
"get_subtree": "Ottieni sottostruttura",
"load_skill": "Carica skill",
"web_search": "Ricerca sul web",
"note_in_parent": "<Note/> in <Parent/>",
"get_attachment": "Scarica l'allegato",
"get_attachment_content": "Leggi il contenuto dell'allegato"
}
},
"ocr": {
"extracted_text": "Testo estratto (OCR)",
"extracted_text_title": "Testo estratto (OCR)",
"loading_text": "Caricamento del testo OCR in corso...",
"no_text_available": "Non è disponibile alcun testo OCR",
"no_text_explanation": "Questo documento non è stato sottoposto a elaborazione OCR per l'estrazione del testo oppure non è stato trovato alcun testo.",
"failed_to_load": "Impossibile caricare il testo OCR",
"process_now": "Elaborazione OCR",
"processing": "Elaborazione in corso...",
"processing_started": "L'elaborazione OCR è stata avviata. Attendere qualche istante e aggiorna.",
"processing_complete": "Elaborazione OCR completata.",
"processing_failed": "Impossibile avviare l'elaborazione OCR",
"text_filtered_low_confidence": "L'OCR ha rilevato il testo con un livello di affidabilità del {{confidence}}%, ma è stato scartato perché la soglia minima impostata è del {{threshold}}%.",
"open_media_settings": "Apri Impostazioni",
"view_extracted_text": "Visualizza il testo estratto (OCR)"
},
"mind-map": {
"addChild": "Aggiungi figlio",
"addParent": "Aggiungi genitore",
"addSibling": "Aggiungi un fratello o una sorella",
"removeNode": "Rimuovi nodo",
"focus": "Modalità Focus",
"cancelFocus": "Annulla modalità Focus",
"moveUp": "Sposta su",
"moveDown": "Sposta giù",
"link": "Collegamento",
"linkBidirectional": "Collegamento bidirezionale",
"clickTips": "Clicca sul nodo di destinazione",
"summary": "Sommario"
}
}

View File

@@ -1027,12 +1027,15 @@
"spellcheck": {
"title": "スペルチェック",
"description": "これらのオプションはデスクトップビルドにのみ適用され、ブラウザはそれぞれのネイティブスペルチェックを使用します。",
"enable": "スペルチェックを有効",
"language_code_label": "言語コード",
"language_code_placeholder": "例えば \"en-US\", \"de-AT\"",
"multiple_languages_info": "複数の言語はカンマで区切ることができます。例: \"en-US, de-DE, cs\"。 ",
"available_language_codes_label": "使用可能な言語コード:",
"restart-required": "スペルチェックオプションの変更は、アプリケーションの再起動後に有効になります。"
"enable": "スペルチェック",
"language_code_label": "スペルチェック対応言語",
"restart-required": "スペルチェックオプションの変更は、アプリケーションの再起動後に有効になります。",
"custom_dictionary_title": "カスタム辞書",
"custom_dictionary_description": "辞書に追加した単語は、すべてのデバイス間で同期されます。",
"custom_dictionary_edit": "カスタム単語",
"custom_dictionary_edit_description": "スペルチェッカーでエラーとして検出されないようにする単語リストを編集します。変更は再起動後に反映されます。",
"custom_dictionary_open": "辞書の編集",
"related_description": "スペルチェック対応言語とカスタム辞書を設定します。"
},
"sync_2": {
"config_title": "同期設定",
@@ -1570,7 +1573,8 @@
"click_on_canvas_to_place_new_note": "キャンバスをクリックして新しいノートを配置",
"connection_exists": "これらのノート間の接続 '{{name}}' は既に存在します。",
"start_dragging_relations": "ここからリレーションをドラッグして、別のノートにドロップします。",
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。"
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。",
"rename_relation": "リレーション名の変更"
},
"database_anonymization": {
"title": "データベースの匿名化",
@@ -2234,7 +2238,9 @@
"sample_user_journey": "ユーザージャーニー図",
"sample_xy": "XY チャート",
"sample_venn": "ベン図",
"sample_ishikawa": "石川図"
"sample_ishikawa": "石川図",
"sample_treeview": "ツリービュー",
"sample_wardley": "ウォードリーマップ"
},
"llm_chat": {
"placeholder": "メッセージを入力してください…",
@@ -2344,6 +2350,9 @@
"processing": "処理中…",
"processing_started": "OCR 処理が開始されました。しばらくお待ちいただき、ページを更新してください。",
"processing_failed": "OCR 処理の開始に失敗しました",
"view_extracted_text": "抽出されたテキストOCRを表示"
"view_extracted_text": "抽出されたテキストOCRを表示",
"processing_complete": "OCR 処理が完了しました。",
"text_filtered_low_confidence": "OCR は {{confidence}}% の信頼度でテキストを検出しましたが、最小しきい値が {{threshold}}% であるため、破棄されました。",
"open_media_settings": "設定を開く"
}
}

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "Custom script laden mislukt",
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
"message": "Script voor de notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
},
"scripting-error": "Error met script: {{title}}",
"widget-list-error": {

View File

@@ -1665,9 +1665,6 @@
"description": "Te opcje dotyczą tylko wersji desktopowych, przeglądarki będą używać własnego natywnego sprawdzania pisowni.",
"enable": "Włącz sprawdzanie pisowni",
"language_code_label": "Kod(y) języka",
"language_code_placeholder": "na przykład \"pl-PL\", \"en-US\"",
"multiple_languages_info": "Wiele języków można oddzielić przecinkiem, np. \"en-US, de-DE, pl\". ",
"available_language_codes_label": "Dostępne kody języków:",
"restart-required": "Zmiany w opcjach sprawdzania pisowni wejdą w życie po ponownym uruchomieniu aplikacji."
},
"sync_2": {

View File

@@ -1435,9 +1435,6 @@
"description": "Estas opções aplicam-se apenas às versões desktop; os navegadores usarão a sua própria verificação ortográfica nativa.",
"enable": "Ativar verificação ortográfica",
"language_code_label": "Código(s) de idioma",
"language_code_placeholder": "por exemplo \"en-US\", \"de-AT\", \"pt-BR\"",
"multiple_languages_info": "Múltiplos idiomas podem ser separados por vírgula, por exemplo: \"en-US, de-DE, pt-BR, cs\". ",
"available_language_codes_label": "Códigos de idioma disponíveis:",
"restart-required": "As alterações nas opções de verificação ortográfica terão efeito após reiniciar a aplicação."
},
"sync_2": {

View File

@@ -1944,9 +1944,6 @@
"description": "Estas opções se aplicam apenas às versões desktop; os navegadores usarão sua própria verificação ortográfica nativa.",
"enable": "Habilitar verificação ortográfica",
"language_code_label": "Código(s) de idioma",
"language_code_placeholder": "por exemplo \"en-US\", \"de-AT\", \"pt-BR\"",
"multiple_languages_info": "Múltiplos idiomas podem ser separados por vírgula, por exemplo: \"en-US, de-DE, pt-BR, cs\". ",
"available_language_codes_label": "Códigos de idioma disponíveis:",
"restart-required": "As alterações nas opções de verificação ortográfica terão efeito após reiniciar o aplicativo."
},
"sync_2": {

View File

@@ -1237,12 +1237,9 @@
"title": "titlu"
},
"spellcheck": {
"available_language_codes_label": "Coduri de limbă disponibile:",
"description": "Aceste opțiuni se aplică doar pentru aplicația de desktop, navigatoarele web folosesc propriile corectoare ortografice.",
"enable": "Activează corectorul ortografic",
"language_code_label": "Codurile de limbă",
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
"title": "Corector ortografic",
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
},

View File

@@ -1679,10 +1679,7 @@
"title": "Проверка орфографии",
"enable": "Включить проверку орфографии",
"language_code_label": "Код(ы) языков",
"multiple_languages_info": "Несколько языков можно разделять запятой, например, \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Доступные коды языков:",
"restart-required": "Изменения параметров проверки орфографии вступят в силу после перезапуска приложения.",
"language_code_placeholder": "например \"en-US\", \"de-AT\"",
"description": "Эти параметры применимы только для десктопных сборок, браузеры будут использовать собственную встроенную проверку орфографии."
},
"attribute_editor": {

View File

@@ -23,10 +23,33 @@
"close": "Kapat",
"delete_notes_preview": "Not önizlemesini sil",
"delete_all_clones_description": "Tüm klonları da sil (son değişikliklerden geri alınabilir)",
"erase_notes_description": "Normal (yazılımsal) silme işlemi, notları yalnızca silinmiş olarak işaretler ve belirli bir süre içinde (son değişiklikler iletişim kutusunda) geri alınabilir. Bu seçeneği işaretlemek, notları hemen siler ve notların geri alınması mümkün olmaz."
"erase_notes_description": "Normal (yazılımsal) silme işlemi, notları yalnızca silinmiş olarak işaretler ve belirli bir süre içinde (son değişiklikler iletişim kutusunda) geri alınabilir. Bu seçeneği işaretlemek, notları hemen siler ve notların geri alınması mümkün olmaz.",
"erase_notes_warning": "Notları, tüm kopyaları da dahil olmak üzere kalıcı olarak silin (geri alınamaz). Bu işlem, uygulamanın yeniden yüklenmesine neden olacaktır.",
"notes_to_be_deleted": "Aşağıdaki notlar silinecektir. ({{notesCount}})",
"no_note_to_delete": "Hiçbir not silinmeyecek (sadece kopyaları silinecek).",
"broken_relations_to_be_deleted": "Aşağıdaki ilişkiler koparılacak ve silinecektir ({{ relationCount}})",
"cancel": "İptal",
"ok": "Tamam",
"deleted_relation_text": "{{- note}} (silinecek) notu, {{- source}} kaynağından kaynaklanan {{- relation}} ilişkisi tarafından referans alınmaktadır."
},
"export": {
"close": "Kapat"
"close": "Kapat",
"export_note_title": "Notu dışa aktar",
"export_type_subtree": "Bu not ve tüm torunları",
"format_html": "HTML - tüm biçimlendirmeyi koruduğu için önerilir",
"format_html_zip": "ZIP arşivindeki HTML dosyaları - tüm biçimlendirmeyi koruduğu için bu yöntem önerilir.",
"format_markdown": "Markdown - bu, biçimlendirmenin büyük kısmını korur.",
"format_opml": "OPML - yalnızca metin için anahat değişim biçimi. Biçimlendirme, resimler ve dosyalar dahil edilmez.",
"opml_version_1": "OPML v1.0 - yalnızca düz metin",
"opml_version_2": "OPML v2.0 - HTML de destekler",
"export_type_single": "Yalnızca bu not, alt öğeleri olmadan",
"export": "Dışa aktar",
"choose_export_type": "Lütfen önce dışa aktarma türünü seçin",
"export_status": "Dışa aktarma durumu",
"export_in_progress": "Dışa aktarma devam ediyor: {{progressCount}}",
"export_finished_successfully": "Dışa aktarma başarıyla tamamlandı.",
"format_pdf": "PDF - yazdırma veya paylaşım amaçları için.",
"share-format": "Web yayını için HTML - paylaşılan notlarda kullanılan temayı kullanır, ancak statik bir web sitesi olarak yayınlanabilir."
},
"import": {
"chooseImportFile": "İçe aktarım dosyası",
@@ -58,7 +81,9 @@
"widget-render-error": {
"title": "Özel React widget'ı çizilirken sorun yaşandı"
},
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}"
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}",
"widget-missing-parent": "Özel widget'ın zorunlu '{{property}}' özelliği tanımlanmamıştır.\n\nBu komut dosyasının bir kullanıcı arayüzü öğesi olmadan çalıştırılması gerekiyorsa, bunun yerine '#run=frontendStartup' kullanın.",
"open-script-note": "Komut dosyası notunu aç"
},
"add_link": {
"add_link": "Bağlantı ekle",
@@ -103,5 +128,32 @@
"are_you_sure_remove_note": "\"{{title}}\" notunu ilişki haritasından kaldırmak istediğinize emin misiniz?. ",
"also_delete_note": "Notu da sil",
"if_you_dont_check": "Bunu işaretlemezseniz, not yalnızca ilişki haritasından kaldırılacaktır."
},
"help": {
"title": "Özet tablo",
"editShortcuts": "Klavye kısayollarını düzenle",
"noteNavigation": "Not içinde gezinme",
"goUpDown": "Notlar listesinde yukarı/aşağı gitmek",
"collapseExpand": "düğümü daralt/genişlet",
"notSet": "ayarlanmamış",
"goBackForwards": "tarihte geri/ileri git",
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Şuraya Git\" iletişim kutusunu göster</a>",
"scrollToActiveNote": "Aktif nota kaydır",
"jumpToParentNote": "Üst nota git",
"collapseWholeTree": "Tüm not ağacını daralt",
"collapseSubTree": "Alt ağacı daralt",
"tabShortcuts": "Sekme kısayolları",
"newTabNoteLink": "Not bağlantısı notu yeni sekmede açılır",
"newTabWithActivationNoteLink": "Not bağlantısına tıklandığında not yeni bir sekmede açılır ve etkinleştirilir",
"onlyInDesktop": "Yalnızca masaüstünde (Electron derlemesi)",
"openEmptyTab": "boş sekmeyi aç",
"closeActiveTab": "aktif sekmeyi kapat",
"activateNextTab": "sonraki sekmeyi etkinleştir",
"activatePreviousTab": "önceki sekmeyi etkinleştir",
"creatingNotes": "Not oluşturma",
"createNoteAfter": "etkin nottan sonra yeni not oluşturma",
"createNoteInto": "aktif nota yeni bir alt not oluşturun",
"editBranchPrefix": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">prefix</a> değerini aktif not klonunun düzenle",
"movingCloningNotes": "Notları taşıma / klonlama"
}
}

View File

@@ -368,7 +368,7 @@
"calendar_root": "標記應用作為每日筆記的根。只應標記一個筆記。",
"archived": "含有此標籤的筆記預設在搜尋結果中不可見(也適用於跳轉至、新增連結對話方塊等)。",
"exclude_from_export": "筆記(及其子階層)不會包含在任何匯出的筆記中",
"run": "定義腳本應運行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端啟動時或重新整理時但不會在移動端執行。</li>\n<li>mobileStartup - Trilium前端啟動時或重新整理時 在行動端會執行。</li>\n<li>backendStartup - Trilium後端啟動時</li>\n<li>hourly - 每小時運行一次。您可以使用附加標籤<code>runAtHour</code>指定小時。</li>\n<li>daily - 每天運行一次</li>\n</ul>",
"run": "定義腳本應運行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端啟動時或重新整理時但不會在移動端執行。</li>\n<li>mobileStartup - Trilium前端啟動時或重新整理時 在行動端會執行。</li>\n<li>backendStartup - Trilium後端啟動時</li>\n<li>hourly - 每小時運行一次。您可以使用附加標籤<code>runAtHour</code>指定小時。</li>\n<li>daily - 每天運行一次</li>\n</ul>",
"run_on_instance": "定義應在哪個 Trilium 實例上運行。預設為所有實例。",
"run_at_hour": "應在哪個小時運行。應與<code>#run=hourly</code>一起使用。可以多次定義,以便一天內運行多次。",
"disable_inclusion": "含有此標籤的腳本不會包含在父腳本執行中。",
@@ -706,7 +706,8 @@
"export_as_image": "匯出為圖片",
"export_as_image_png": "PNG (點陣)",
"export_as_image_svg": "SVG (向量)",
"note_map": "筆記地圖"
"note_map": "筆記地圖",
"view_ocr_text": "顯示 OCR 文字"
},
"onclick_button": {
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
@@ -1196,12 +1197,28 @@
},
"images": {
"images_section_title": "圖片",
"download_images_automatically": "自動下載圖片以供離線使用。",
"download_images_description": "貼上的 HTML 可能包含線上圖片的引用Trilium 會找到這些引用並下載圖片,以便它們可以離線使用。",
"enable_image_compression": "啟用圖片壓縮",
"max_image_dimensions": "圖片的最大寬度 / 高度(超過此限制的圖片將會被縮放)。",
"jpeg_quality_description": "JPEG 質量10 - 最差質量100 最佳質量,建議為 50 - 85",
"max_image_dimensions_unit": "像素"
"download_images_automatically": "自動下載圖片",
"download_images_description": "貼上的 HTML 下載引用的線上圖片以便離線使用。",
"enable_image_compression": "圖片壓縮",
"max_image_dimensions": "最大圖片尺寸",
"jpeg_quality_description": "建議範圍為 5085。較低的數值可縮小檔案大小較高的數值則能保留更多細節。",
"max_image_dimensions_unit": "像素",
"enable_image_compression_description": "在上傳或貼上圖片時壓縮並調整圖片大小。",
"max_image_dimensions_description": "超過此尺寸的圖片將會自動調整大小。",
"jpeg_quality": "JPEG 品質",
"ocr_section_title": "文字擷取OCR",
"ocr_related_content_languages": "內容語言(用於文字擷取)",
"ocr_auto_process": "自動處理新檔案",
"ocr_auto_process_description": "自動從新上傳或貼上的檔案中擷取文字。",
"ocr_min_confidence": "最低信賴度",
"ocr_confidence_description": "僅提取高於此信賴度閾值的文字。較低的閾值雖能包含更多文字,但準確度可能較低。",
"batch_ocr_title": "處理現有檔案",
"batch_ocr_description": "從筆記中的所有現有圖片、PDF 檔案及 Office 文件中擷取文字。根據檔案數量多寡,此過程可能需要一些時間。",
"batch_ocr_start": "開始批次處理",
"batch_ocr_starting": "開始批次處理…",
"batch_ocr_progress": "正在處理 {{processed}} 個檔案,共 {{total}} 個檔案…",
"batch_ocr_completed": "批次處理完成!已處理 {{processed}} 個檔案。",
"batch_ocr_error": "批次處理期間發生錯誤:{{error}}"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "附件清理超時",
@@ -1381,9 +1398,6 @@
"description": "這些選項僅適用於桌面版,瀏覽器將使用其原生的拼寫檢查功能。",
"enable": "啟用拼寫檢查",
"language_code_label": "語言代碼",
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
"multiple_languages_info": "多種語言可以用逗號分隔,例如 \"en-US, de-DE, cs\"。 ",
"available_language_codes_label": "可用的語言代碼:",
"restart-required": "拼寫檢查選項的更改將在應用重啟後生效。"
},
"sync_2": {
@@ -1497,7 +1511,8 @@
"new-feature": "新增",
"collections": "集合",
"ai-chat": "AI 聊天",
"spreadsheet": "試算表"
"spreadsheet": "試算表",
"llm-chat": "AI 對話"
},
"protect_note": {
"toggle-on": "保護筆記",
@@ -1866,7 +1881,7 @@
},
"content_language": {
"title": "內文語言",
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左之類的功能。"
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左及文字擷取 (OCR) 等功能。"
},
"switch_layout_button": {
"title_vertical": "將編輯面板移至底部",
@@ -2046,7 +2061,9 @@
"title": "實驗性選項",
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
"new_layout_name": "新版面配置",
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。",
"llm_name": "AI / LLM 對話",
"llm_description": "啟用由大語言模型驅動的 AI 聊天側邊欄及 LLM 聊天筆記。"
},
"server": {
"unknown_http_error_title": "與伺服器通訊錯誤",
@@ -2229,6 +2246,121 @@
"sample_user_journey": "使用者旅程",
"sample_xy": "XY 圖表",
"sample_venn": "韋恩圖",
"sample_ishikawa": "魚骨圖"
"sample_ishikawa": "魚骨圖",
"sample_treeview": "樹狀視圖",
"sample_wardley": "沃德利地圖"
},
"llm_chat": {
"placeholder": "輸入訊息…",
"send": "送出",
"sending": "正在送出…",
"empty_state": "請在下方輸入訊息,開啟對話。",
"searching_web": "正在搜尋網頁…",
"web_search": "網頁搜尋",
"note_tools": "筆記存取",
"sources": "來源",
"sources_summary": "來自 {{sites}} 個網站的 {{count}} 個來源",
"extended_thinking": "延伸思考",
"legacy_models": "傳統模型",
"thinking": "正在思考…",
"thought_process": "思考過程",
"tool_calls": "{{count}} 次工具調用",
"input": "輸入",
"result": "結果",
"error": "錯誤",
"tool_error": "失敗",
"total_tokens": "{{total}} 個詞元",
"tokens_detail": "{{prompt}} 提示詞 + {{completion}} 補全",
"tokens_used": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
"tokens_used_with_cost": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}}",
"tokens_used_with_model": "{{model}}{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
"tokens_used_with_model_and_cost": "{{model}}{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}}",
"tokens": "詞元",
"context_used": "已使用 {{percentage}}%",
"note_context_enabled": "點擊以禁用筆記上下文:{{title}}",
"note_context_disabled": "點擊將當前筆記納入上下文",
"no_provider_message": "尚未設定任何 AI 服務提供者。請新增一個以開始聊天。",
"add_provider": "新增 AI 提供者"
},
"ocr": {
"processing_complete": "OCR 處理已完成。",
"processing_failed": "無法啟動 OCR 處理",
"text_filtered_low_confidence": "OCR 偵測到的信賴度為 {{confidence}}%,但因您的最低閾值設定為 {{threshold}}%,故該結果已被捨棄。",
"open_media_settings": "開啟設定",
"view_extracted_text": "檢視擷取的文字 (OCR)",
"extracted_text": "已擷取的文字 (OCR)",
"extracted_text_title": "已擷取的文字 (OCR)",
"loading_text": "正在載入 OCR 文字…",
"no_text_available": "無 OCR 文字可用",
"no_text_explanation": "此筆記尚未經過 OCR 文字擷取處理,或未找到任何文字。",
"failed_to_load": "載入 OCR 文字失敗",
"process_now": "處理 OCR",
"processing": "正在處理…",
"processing_started": "OCR 處理已開始。請稍候片刻並重新整理頁面。"
},
"mind-map": {
"addChild": "新增子節點",
"addParent": "新增父節點",
"addSibling": "新增同級節點",
"removeNode": "刪除節點",
"focus": "專注模式",
"cancelFocus": "退出專注模式",
"moveUp": "上移",
"moveDown": "下移",
"link": "連結",
"linkBidirectional": "雙向連結",
"clickTips": "請點擊目標節點",
"summary": "摘要"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "設定 AI 及大型語言模型整合。",
"feature_not_enabled": "請前往「設定」→「進階」→「實驗性功能」啟用 LLM 實驗性功能,即可使用 AI 整合。",
"add_provider": "新增提供者",
"add_provider_title": "新增 AI 提供者",
"configured_providers": "已設定的提供者",
"no_providers_configured": "尚未設定任何提供者。",
"provider_name": "名稱",
"provider_type": "提供者",
"actions": "動作",
"delete_provider": "刪除",
"delete_provider_confirmation": "您確定要刪除提供者 \"{{name}}\" 嗎?",
"api_key": "API 金鑰",
"api_key_placeholder": "請輸入您的 API 金鑰",
"cancel": "取消",
"mcp_title": "MCP模型上下文協定",
"mcp_enabled": "MCP 伺服器",
"mcp_enabled_description": "公開一個模型上下文協定 (MCP) 端點,以便人工智慧編程助手(例如 Claude Code、GitHub Copilot能夠讀取並修改您的筆記。此端點僅限從 localhost 存取。",
"mcp_endpoint_title": "端點網址",
"mcp_endpoint_description": "將此網址新增至您的 AI 助理的 MCP 設定中",
"tools": {
"search_notes": "搜尋筆記",
"get_note": "取得筆記",
"get_note_content": "取得筆記內容",
"update_note_content": "更新筆記內容",
"append_to_note": "追加至筆記",
"create_note": "建立筆記",
"get_attributes": "取得屬性",
"get_attribute": "取得屬性",
"set_attribute": "設定屬性",
"delete_attribute": "移除屬性",
"get_child_notes": "取得子筆記",
"get_subtree": "取得子階層",
"load_skill": "載入技能",
"web_search": "網頁搜尋",
"note_in_parent": "<Note/> 於 <Parent/>",
"get_attachment": "取得附件",
"get_attachment_content": "讀取附件內容"
}
},
"sidebar_chat": {
"title": "AI 對話",
"launcher_title": "打開 AI 對話",
"new_chat": "開始新對話",
"save_chat": "將對話保存至筆記",
"empty_state": "開始會話",
"history": "對話歷史",
"recent_chats": "最近的對話",
"no_chats": "無先前的對話記錄"
}
}

View File

@@ -1744,9 +1744,6 @@
"description": "Ці параметри застосовуються лише для збірок для ПК, браузери використовуватимуть власну вбудовану перевірку орфографії.",
"enable": "Увімкнути перевірку орфографії",
"language_code_label": "Код(и) мови",
"language_code_placeholder": "наприклад, \"en-US\", \"de-AT\"",
"multiple_languages_info": "Кілька мов можна розділяти комами, наприклад, \"en-US, de-DE, cs\". ",
"available_language_codes_label": "Доступні коди мови:",
"restart-required": "Зміни в параметрах перевірки орфографії набудуть чинності після перезапуску програми."
},
"sync_2": {

View File

@@ -66,6 +66,7 @@ declare module "preact" {
interface ElectronWebViewElement extends JSX.HTMLAttributes<HTMLElement> {
src: string;
class: string;
key?: string | number;
}
interface IntrinsicElements {

View File

@@ -24,6 +24,7 @@ interface CustomGlobals {
getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: () => FNote | null;
getThemeStyle: () => "auto" | "light" | "dark";
ESLINT: Library;
appContext: AppContext;
froca: Froca;
@@ -51,8 +52,9 @@ interface CustomGlobals {
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
themeCssUrl: string;
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
theme: string;
themeBase?: "next" | "next-light" | "next-dark";
customThemeCssUrl?: string;
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";

View File

@@ -1,20 +1,9 @@
:root {
/* Default values to be overridden by themes */
--calendar-coll-event-background-lightness: 95%;
--calendar-coll-event-background-saturation: 80%;
--calendar-coll-event-background-color: var(--accented-background-color);
--calendar-coll-event-text-color: var(--main-text-color);
--calendar-coll-event-hover-filter: none;
--callendar-coll-event-archived-sripe-color: #00000013;
--calendar-coll-today-background-color: var(--more-accented-background-color);
}
.calendar-view {
--fc-event-border-color: var(--calendar-coll-event-text-color);
--fc-event-bg-color: var(--calendar-coll-event-background-color);
--fc-event-text-color: var(--calendar-coll-event-text-color);
--fc-event-border-color: var(--calendar-coll-event-text-color, var(--main-text-color));
--fc-event-bg-color: var(--calendar-coll-event-background-color, var(--accented-background-color));
--fc-event-text-color: var(--calendar-coll-event-text-color, var(--main-text-color));
--fc-event-selected-overlay-color: transparent;
--fc-today-bg-color: var(--calendar-coll-today-background-color);
--fc-today-bg-color: var(--calendar-coll-today-background-color, var(--more-accented-background-color));
overflow: hidden;
position: relative;
@@ -123,7 +112,7 @@
z-index: -1;
--c1: transparent;
--c2: var(--callendar-coll-event-archived-sripe-color);
--c2: var(--callendar-coll-event-archived-sripe-color, #00000013);
background: repeating-linear-gradient(45deg, var(--c1), var(--c1) 8px,
var(--c2) 8px, var(--c2) 16px);
@@ -153,8 +142,8 @@
--fc-event-text-color: var(--custom-color);
--fc-event-bg-color: hsl(var(--custom-color-hue),
var(--calendar-coll-event-background-saturation),
var(--calendar-coll-event-background-lightness)) !important;
var(--calendar-coll-event-background-saturation, 80%),
var(--calendar-coll-event-background-lightness, 95%)) !important;
}
.calendar-view a.fc-timegrid-event:focus-visible,
@@ -171,7 +160,7 @@
.calendar-view a.fc-timegrid-event:hover,
.calendar-view a.fc-daygrid-event:hover {
filter: var(--calendar-coll-event-hover-filter);
filter: var(--calendar-coll-event-hover-filter, none);
border-color: var(--fc-event-text-color);
text-decoration: none;
color: currentColor;

View File

@@ -82,6 +82,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
hi: () => import("@fullcalendar/core/locales/hi"),
ga: null,
cn: () => import("@fullcalendar/core/locales/zh-cn"),
cs: () => import("@fullcalendar/core/locales/cs"),
tw: () => import("@fullcalendar/core/locales/zh-tw"),
ro: () => import("@fullcalendar/core/locales/ro"),
ru: () => import("@fullcalendar/core/locales/ru"),

View File

@@ -1,18 +1,21 @@
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import "./NoteMap.css";
import { getThemeStyle, MapType, NoteMapWidgetMode, rgb2hex } from "./utils";
import { RefObject } from "preact";
import FNote from "../../entities/fnote";
import { useElementSize, useNoteLabel } from "../react/hooks";
import ForceGraph from "force-graph";
import { RefObject } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import link_context_menu from "../../menus/link_context_menu";
import hoisted_note from "../../services/hoisted_note";
import { t } from "../../services/i18n";
import { getEffectiveThemeStyle } from "../../services/theme";
import ActionButton from "../react/ActionButton";
import { useElementSize, useNoteLabel } from "../react/hooks";
import Slider from "../react/Slider";
import { loadNotesAndRelations, NoteMapLinkObject, NoteMapNodeObject, NotesAndRelationsData } from "./data";
import { CssData, setupRendering } from "./rendering";
import ActionButton from "../react/ActionButton";
import { t } from "../../services/i18n";
import link_context_menu from "../../menus/link_context_menu";
import appContext from "../../components/app_context";
import Slider from "../react/Slider";
import hoisted_note from "../../services/hoisted_note";
import { MapType, NoteMapWidgetMode, rgb2hex } from "./utils";
interface NoteMapProps {
note: FNote;
@@ -40,9 +43,9 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
return hoisted_note.getHoistedNoteId();
} else if (mapRootIdLabel) {
return mapRootIdLabel;
} else {
return appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
}
return appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
}, [ note ]);
// Build the note graph instance.
@@ -67,7 +70,7 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
noteIdToSizeMap: notesAndRelations.noteIdToSizeMap,
cssData,
notesAndRelations,
themeStyle: getThemeStyle(),
themeStyle: getEffectiveThemeStyle(),
widgetMode,
mapType
});
@@ -113,7 +116,7 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
node.fx = undefined;
node.fy = undefined;
}
})
});
}, [ fixNodes, mapType ]);
return (
@@ -159,7 +162,7 @@ function MapTypeSwitcher({ icon, text, type, currentMapType, setMapType }: {
onClick={() => setMapType(type)}
frame
/>
)
);
}
function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData {
@@ -170,5 +173,5 @@ function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData
fontFamily: containerStyle.fontFamily,
textColor: rgb2hex(containerStyle.color),
mutedTextColor: rgb2hex(styleResolverStyle.color)
}
};
}

View File

@@ -27,7 +27,3 @@ export function generateColorFromString(str: string, themeStyle: "light" | "dark
return color;
}
export function getThemeStyle() {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as "light" | "dark";
}

View File

@@ -1,13 +1,14 @@
import BasicWidget from "./basic_widget.js";
import server from "../services/server.js";
import linkService from "../services/link.js";
import froca from "../services/froca.js";
import utils, { handleRightToLeftPlacement } from "../services/utils.js";
import appContext from "../components/app_context.js";
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
import { t } from "../services/i18n.js";
import { Dropdown, Tooltip } from "bootstrap";
import appContext from "../components/app_context.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import linkService from "../services/link.js";
import server from "../services/server.js";
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
import utils, { handleRightToLeftPlacement } from "../services/utils.js";
import BasicWidget from "./basic_widget.js";
const TPL = /*html*/`
<div class="quick-search input-group input-group-sm">
<style>
@@ -245,7 +246,7 @@ export default class QuickSearchWidget extends BasicWidget {
const { searchResultNoteIds, searchResults, error } = await server.get<QuickSearchResponse>(`quick-search/${encodeURIComponent(searchString)}`);
if (error) {
let tooltip = new Tooltip(this.$searchString[0], {
const tooltip = new Tooltip(this.$searchString[0], {
trigger: "manual",
title: `Search error: ${error}`,
placement: handleRightToLeftPlacement("right")
@@ -289,10 +290,9 @@ export default class QuickSearchWidget extends BasicWidget {
const resultsToDisplay = this.allSearchResults.slice(startIndex, endIndex);
for (const result of resultsToDisplay) {
const noteId = result.notePath.split("/").pop();
if (!noteId) continue;
if (!result.notePath) continue;
const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');
const $item = $(`<a class="dropdown-item" tabindex="0" href="#${result.notePath}">`);
// Build the display HTML with content snippet below the title
let itemHtml = `<div class="quick-search-item">
@@ -317,23 +317,13 @@ export default class QuickSearchWidget extends BasicWidget {
$item.html(itemHtml);
$item.on("click", (e) => {
$item.on("click auxclick", () => {
this.dropdown.hide();
e.preventDefault();
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(noteId);
}
});
shortcutService.bindElShortcut($item, "return", () => {
this.dropdown.hide();
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(noteId);
}
$item[0].click();
});
this.$dropdownMenu.append($item);
@@ -350,24 +340,18 @@ export default class QuickSearchWidget extends BasicWidget {
const $link = await linkService.createLink(note.noteId, { showNotePath: true, showNoteIcon: true });
$link.addClass("dropdown-item");
$link.attr("tabIndex", "0");
$link.on("click", (e) => {
$link.on("click auxclick", (e) => {
this.dropdown.hide();
if (!e.target || e.target.nodeName !== "A") {
// click on the link is handled by link handling, but we want the whole item clickable
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
if (!e.target || (e.target as HTMLElement).nodeName !== "A") {
// click on the <a> is handled by the global goToLink handler,
// but we want the whole item clickable
$link.find("a")[0]?.dispatchEvent(new MouseEvent(e.type, e.originalEvent as MouseEventInit));
}
});
shortcutService.bindElShortcut($link, "return", () => {
this.dropdown.hide();
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
activeContext.setNote(note.noteId);
}
$link.find("a")[0]?.click();
});
this.$dropdownMenu.append($link);

View File

@@ -1385,7 +1385,7 @@ export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
}
export function useColorScheme() {
const themeStyle = getThemeStyle();
const themeStyle = window.glob.getThemeStyle();
const defaultValue = themeStyle === "auto" ? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) : themeStyle === "dark";
const [ prefersDark, setPrefersDark ] = useState(defaultValue);
@@ -1400,12 +1400,3 @@ export function useColorScheme() {
return prefersDark ? "dark" : "light";
}
function getThemeStyle() {
const style = window.getComputedStyle(document.body);
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
return themeStyle as "light" | "dark";
}
return "auto";
}

View File

@@ -903,7 +903,7 @@ export default class TabRowWidget extends BasicWidget {
loadResults.isNoteReloaded(noteContext.noteId) ||
loadResults
.getAttributeRows()
.find((attr) => ["workspace", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name || "") && attributeService.isAffecting(attr, noteContext.note))
.find((attr) => ["workspace", "iconClass", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name || "") && attributeService.isAffecting(attr, noteContext.note))
) {
const $tab = this.getTabById(noteContext.ntxId);

View File

@@ -57,6 +57,7 @@ function DesktopWebView({ src, ntxId }: { src: string, ntxId: string | null | un
return <webview
ref={webviewRef}
src={src}
key={src}
class="note-detail-web-view-content"
/>;
}
@@ -80,6 +81,7 @@ function BrowserWebView({ src, ntxId }: { src: string, ntxId: string | null | un
return <iframe
ref={iframeRef}
src={src}
key={src}
class="note-detail-web-view-content"
sandbox="allow-same-origin allow-scripts allow-popups" />;
}

View File

@@ -4,6 +4,7 @@ import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"] | null> = {
ar: "ar-SA",
cn: "zh-CN",
cs: "cs-CZ",
de: "de-DE",
en: "en",
"en-GB": "en",

View File

@@ -45,3 +45,15 @@
.option-row.centered {
justify-content: center;
}
.option-row-link.use-tn-links {
text-decoration: none;
color: inherit;
margin-inline: calc(-1 * var(--options-card-padding, 15px));
padding-inline: var(--options-card-padding, 15px);
transition: background-color 250ms ease-in-out;
}
.option-row-link:hover {
background: var(--hover-item-background-color);
}

View File

@@ -1,5 +1,7 @@
import { cloneElement, VNode } from "preact";
import "./OptionsRow.css";
import { cloneElement, VNode } from "preact";
import { useUniqueName } from "../../../react/hooks";
interface OptionsRowProps {
@@ -25,4 +27,24 @@ export default function OptionsRow({ name, label, description, children, centere
</div>
</div>
);
}
}
interface OptionsRowLinkProps {
label: string;
description?: string;
href: string;
}
export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) {
return (
<a href={href} className="option-row option-row-link use-tn-links no-tooltip-preview">
<div className="option-row-label">
<label style={{ cursor: "pointer" }}>{label}</label>
{description && <small className="option-row-description">{description}</small>}
</div>
<div className="option-row-input">
<span className="bx bx-chevron-right" />
</div>
</a>
);
}

View File

@@ -1,24 +1,36 @@
import OptionsSection from "./OptionsSection";
import type { OptionPages } from "../../ContentWidget";
import { t } from "../../../../services/i18n";
import type { OptionPages } from "../../ContentWidget";
import { OptionsRowLink } from "./OptionsRow";
import OptionsSection from "./OptionsSection";
interface RelatedSettingsItem {
title: string;
description?: string;
targetPage: OptionPages;
enabled?: boolean;
}
interface RelatedSettingsProps {
items: {
title: string;
targetPage: OptionPages;
}[];
items: RelatedSettingsItem[];
}
export default function RelatedSettings({ items }: RelatedSettingsProps) {
const filteredItems = items.filter(item => item.enabled !== false);
if (filteredItems.length === 0) {
return null;
}
return (
<OptionsSection title={t("settings.related_settings")}>
<nav className="use-tn-links" style={{ padding: 0, margin: 0, listStyleType: "none" }}>
{items.map(item => (
<li>
<a href={`#root/_hidden/_options/${item.targetPage}`}>{item.title}</a>
</li>
))}
</nav>
{filteredItems.map((item) => (
<OptionsRowLink
key={item.targetPage}
label={item.title}
description={item.description}
href={`#root/_hidden/_options/${item.targetPage}`}
/>
))}
</OptionsSection>
);
}

View File

@@ -5,13 +5,14 @@ import OptionsRow from "./components/OptionsRow";
import OptionsSection from "./components/OptionsSection";
import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks";
import type { Locale } from "@triliumnext/commons";
import { restartDesktopApp } from "../../../services/utils";
import { isElectron, restartDesktopApp } from "../../../services/utils";
import FormRadioGroup from "../../react/FormRadioGroup";
import FormText from "../../react/FormText";
import RawHtml from "../../react/RawHtml";
import Admonition from "../../react/Admonition";
import Button from "../../react/Button";
import CheckboxList from "./components/CheckboxList";
import RelatedSettings from "./components/RelatedSettings";
import { LocaleSelector } from "./components/LocaleSelector";
export default function InternationalizationOptions() {
@@ -19,8 +20,17 @@ export default function InternationalizationOptions() {
<>
<LocalizationOptions />
<ContentLanguages />
{isElectron() && (
<RelatedSettings items={[
{
title: t("spellcheck.title"),
description: t("spellcheck.related_description"),
targetPage: "_optionsSpellcheck"
}
]} />
)}
</>
)
);
}
function LocalizationOptions() {

View File

@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import { isElectron } from "../../../services/utils";
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
import FormToggle from "../../react/FormToggle";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
@@ -93,7 +94,8 @@ function OcrSettings() {
<RelatedSettings items={[
{
title: t("images.ocr_related_content_languages"),
targetPage: "_optionsLocalization"
targetPage: "_optionsLocalization",
enabled: isElectron(), // This setting is only relevant for desktop, as web browsers use their own native OCR which doesn't support language selection.
}
]} />
</>

View File

@@ -1,63 +1,132 @@
import { useMemo } from "preact/hooks";
import { useCallback, useMemo } from "preact/hooks";
import appContext from "../../../components/app_context";
import { t } from "../../../services/i18n";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils";
import Button from "../../react/Button";
import FormText from "../../react/FormText";
import FormTextBox from "../../react/FormTextBox";
import FormToggle from "../../react/FormToggle";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import NoItems from "../../react/NoItems";
import CheckboxList from "./components/CheckboxList";
import OptionsRow from "./components/OptionsRow";
import OptionsSection from "./components/OptionsSection";
import { dynamicRequire, isElectron } from "../../../services/utils";
export default function SpellcheckSettings() {
if (isElectron()) {
return <ElectronSpellcheckSettings />
} else {
return <WebSpellcheckSettings />
return <ElectronSpellcheckSettings />;
}
return <WebSpellcheckSettings />;
}
interface SpellcheckLanguage {
code: string;
name: string;
}
function ElectronSpellcheckSettings() {
const [ spellCheckEnabled, setSpellCheckEnabled ] = useTriliumOptionBool("spellCheckEnabled");
return (
<>
<OptionsSection title={t("spellcheck.title")}>
<FormText>{t("spellcheck.restart-required")}</FormText>
<OptionsRow name="spell-check-enabled" label={t("spellcheck.enable")}>
<FormToggle
switchOnName="" switchOffName=""
currentValue={spellCheckEnabled}
onChange={setSpellCheckEnabled}
/>
</OptionsRow>
<OptionsRow name="restart" centered>
<Button
name="restart-app-button"
text={t("electron_integration.restart-app-button")}
size="micro"
onClick={restartDesktopApp}
/>
</OptionsRow>
</OptionsSection>
{spellCheckEnabled && <SpellcheckLanguages />}
{spellCheckEnabled && <CustomDictionary />}
</>
);
}
function SpellcheckLanguages() {
const [ spellCheckLanguageCode, setSpellCheckLanguageCode ] = useTriliumOption("spellCheckLanguageCode");
const availableLanguageCodes = useMemo(() => {
const selectedCodes = useMemo(() =>
(spellCheckLanguageCode ?? "")
.split(",")
.map((c) => c.trim())
.filter((c) => c.length > 0),
[spellCheckLanguageCode]
);
const setSelectedCodes = useCallback((codes: string[]) => {
setSpellCheckLanguageCode(codes.join(", "));
}, [setSpellCheckLanguageCode]);
const availableLanguages = useMemo<SpellcheckLanguage[]>(() => {
if (!isElectron()) {
return [];
}
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
return webContents.session.availableSpellCheckerLanguages as string[];
}, [])
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
const codes = webContents.session.availableSpellCheckerLanguages as string[];
const displayNames = new Intl.DisplayNames([navigator.language], { type: "language" });
return codes.map((code) => ({
code,
name: displayNames.of(code) ?? code
})).sort((a, b) => a.name.localeCompare(b.name));
}, []);
return (
<OptionsSection title={t("spellcheck.title")}>
<FormText>{t("spellcheck.restart-required")}</FormText>
<FormCheckbox
name="spell-check-enabled"
label={t("spellcheck.enable")}
currentValue={spellCheckEnabled} onChange={setSpellCheckEnabled}
<OptionsSection title={t("spellcheck.language_code_label")}>
<CheckboxList
values={availableLanguages}
keyProperty="code" titleProperty="name"
currentValue={selectedCodes}
onChange={setSelectedCodes}
columnWidth="200px"
/>
<FormGroup name="spell-check-languages" label={t("spellcheck.language_code_label")} description={t("spellcheck.multiple_languages_info")}>
<FormTextBox
placeholder={t("spellcheck.language_code_placeholder")}
currentValue={spellCheckLanguageCode} onChange={setSpellCheckLanguageCode}
/>
</FormGroup>
<FormText>
<strong>{t("spellcheck.available_language_codes_label")} </strong>
{availableLanguageCodes.join(", ")}
</FormText>
</OptionsSection>
)
);
}
function CustomDictionary() {
function openDictionary() {
appContext.triggerCommand("openInPopup", { noteIdOrPath: "_customDictionary" });
}
return (
<OptionsSection title={t("spellcheck.custom_dictionary_title")}>
<FormText>{t("spellcheck.custom_dictionary_description")}</FormText>
<OptionsRow name="custom-dictionary" label={t("spellcheck.custom_dictionary_edit")} description={t("spellcheck.custom_dictionary_edit_description")}>
<Button
name="open-custom-dictionary"
text={t("spellcheck.custom_dictionary_open")}
icon="bx bx-edit"
onClick={openDictionary}
/>
</OptionsRow>
</OptionsSection>
);
}
function WebSpellcheckSettings() {
return (
<OptionsSection title={t("spellcheck.title")}>
<p>{t("spellcheck.description")}</p>
<OptionsSection>
<NoItems
text={t("spellcheck.description")}
icon="bx bx-check-double"
/>
</OptionsSection>
)
}
);
}

View File

@@ -8,13 +8,11 @@ import { HTMLProps } from "preact/compat";
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import attribute_autocomplete from "../../../services/attribute_autocomplete";
import dialog from "../../../services/dialog";
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import utils from "../../../services/utils";
import ActionButton from "../../react/ActionButton";
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
import { TypeWidgetProps } from "../type_widget";
@@ -23,7 +21,7 @@ import { buildRelationContextMenuHandler } from "./context_menu";
import { JsPlumb } from "./jsplumb";
import { NoteBox } from "./NoteBox";
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
import { getMousePosition, getZoom, idToNoteId, noteIdToId, promptForRelationName } from "./utils";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
@@ -415,27 +413,7 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec
// if there's no event, then this has been triggered programmatically
if (!originalEvent || !mapApiRef.current) return;
const name = await dialog.prompt({
message: t("relation_map.specify_new_relation_name"),
shown: ({ $answer }) => {
if (!$answer) {
return;
}
$answer.on("keyup", () => {
// invalid characters are simply ignored (from user perspective they are not even entered)
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
attribute_autocomplete.initAttributeNameAutocomplete({
$el: $answer,
attributeType: "relation",
open: true
});
}
});
const name = await promptForRelationName();
// Delete the newly created connection if the dialog was dismissed.
if (!name || !name.trim()) {

View File

@@ -75,6 +75,29 @@ export default class RelationMapApi {
this.onDataChange(true);
}
async renameRelation(connection: Connection, newName: string) {
newName = utils.filterAttributeName(newName);
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
if (!relation) return false;
// Check if a relation with the new name already exists between these notes.
const exists = this.relations.some(
(rel) => rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId && rel.name === newName
);
if (exists) return false;
await server.put(`notes/${relation.sourceNoteId}/relations/${newName}/to/${relation.targetNoteId}`);
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
this.onDataChange(true);
return true;
}
getRelationName(connection: Connection): string | undefined {
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
return relation?.name;
}
cleanupOtherNotes(noteIds: string[]) {
const filteredNotes = this.data.notes.filter((note) => noteIds.includes(note.noteId));
if (filteredNotes.length === this.data.notes.length) return;

View File

@@ -9,6 +9,7 @@ import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import RelationMapApi from "./api";
import { promptForRelationName } from "./utils";
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
return (e: MouseEvent) => {
@@ -73,9 +74,25 @@ export function buildRelationContextMenuHandler(connection: Connection, mapApiRe
contextMenu.show({
x: event.pageX,
y: event.pageY,
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
items: [
{ title: t("relation_map.rename_relation"), command: "rename", uiIcon: "bx bx-pencil" },
{ kind: "separator" },
{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }
],
selectMenuItemHandler: async ({ command }) => {
if (command === "remove") {
if (command === "rename") {
const currentName = mapApiRef.current?.getRelationName(connection) ?? "";
const newName = await promptForRelationName(currentName);
if (!newName?.trim() || newName === currentName) {
return;
}
const result = await mapApiRef.current?.renameRelation(connection, newName);
if (!result) {
await dialog.info(t("relation_map.connection_exists", { name: newName }));
}
} else if (command === "remove") {
if (!(await dialog.confirm(t("relation_map.confirm_remove_relation")))) {
return;
}

View File

@@ -1,4 +1,7 @@
import attribute_autocomplete from "../../../services/attribute_autocomplete";
import dialog from "../../../services/dialog";
import { t } from "../../../services/i18n";
import utils from "../../../services/utils";
export function noteIdToId(noteId: string) {
return `rel-map-note-${noteId}`;
@@ -32,3 +35,26 @@ export function getMousePosition(evt: MouseEvent, container: HTMLDivElement, zoo
y: ((evt.clientY ?? 0) - rect.top) / zoom
};
}
export function promptForRelationName(defaultValue?: string): Promise<string | null> {
return dialog.prompt({
message: t("relation_map.specify_new_relation_name"),
defaultValue,
shown: ({ $answer }) => {
if (!$answer) {
return;
}
$answer.on("keyup", () => {
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
attribute_autocomplete.initAttributeNameAutocomplete({
$el: $answer,
attributeType: "relation",
open: true
});
}
});
}

View File

@@ -176,7 +176,9 @@ const config: ForgeConfig = {
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true
[FuseV1Options.OnlyLoadAppFromAsar]: true,
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
}
}
],

View File

@@ -39,12 +39,12 @@
"@electron-forge/maker-zip": "7.11.1",
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
"@electron-forge/plugin-fuses": "7.11.1",
"@electron/fuses": "1.8.0",
"@electron/fuses": "2.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"@types/electron-squirrel-startup": "1.0.2",
"copy-webpack-plugin": "13.0.1",
"electron": "40.8.5",
"copy-webpack-plugin": "14.0.0",
"electron": "41.1.1",
"prebuild-install": "7.1.3"
}
}

View File

@@ -6,6 +6,6 @@
"e2e": "playwright test"
},
"devDependencies": {
"dotenv": "17.4.0"
"dotenv": "17.4.1"
}
}

View File

@@ -1,4 +1,5 @@
import test, { expect } from "@playwright/test";
import App from "./support/app";
test("Native Title Bar not displayed on web", async ({ page, context }) => {
@@ -18,8 +19,6 @@ test("Tray settings not displayed on web", async ({ page, context }) => {
test("Spellcheck settings not displayed on web", async ({ page, context }) => {
const app = new App(page, context);
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck" });
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Spell Check" })).toBeVisible();
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Tray" })).toBeHidden();
await expect(app.currentNoteSplitContent.getByText("These options apply only for desktop builds")).toBeVisible();
await expect(app.currentNoteSplitContent.getByText("Enable spellcheck")).toBeHidden();
await expect(app.currentNoteSplitContent.getByText("Check spelling")).toBeHidden();
});

View File

@@ -30,11 +30,11 @@
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
},
"dependencies": {
"@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/google": "3.0.55",
"@ai-sdk/openai": "3.0.49",
"@ai-sdk/anthropic": "3.0.66",
"@ai-sdk/google": "3.0.58",
"@ai-sdk/openai": "3.0.50",
"@modelcontextprotocol/sdk": "^1.12.1",
"ai": "6.0.142",
"ai": "6.0.146",
"better-sqlite3": "12.8.0",
"html-to-text": "9.0.5",
"js-yaml": "4.1.1",
@@ -131,7 +131,7 @@
"tmp": "0.2.5",
"turnish": "1.8.0",
"unescape": "1.0.1",
"vite": "8.0.3",
"vite": "8.0.5",
"ws": "8.20.0",
"xml2js": "0.6.2",
"yauzl": "3.3.0"

View File

@@ -51,7 +51,8 @@ VERSION=`jq -r ".version" package.json`
ARCHIVE_NAME="TriliumNotes-Server-${VERSION}-linux-${ARCH}"
echo "Creating Archive $ARCHIVE_NAME..."
mkdir $DIST_DIR
rm -rf $DIST_DIR
mkdir -p $DIST_DIR
cp -r "$BUILD_DIR" "$DIST_DIR/$ARCHIVE_NAME"
cd $DIST_DIR
tar cJf "$ARCHIVE_NAME.tar.xz" "$ARCHIVE_NAME"

View File

@@ -10,6 +10,7 @@ import helmet from "helmet";
import { t } from "i18next";
import path from "path";
import favicon from "serve-favicon";
import type serveStatic from "serve-static";
import assets from "./routes/assets.js";
import custom from "./routes/custom.js";
@@ -24,6 +25,9 @@ import { RESOURCE_DIR } from "./services/resource_dir.js";
import sql_init from "./services/sql_init.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
// Allow serving assets even if the installation path contains a hidden (dot-prefixed) directory.
const STATIC_OPTIONS: serveStatic.ServeStaticOptions = { dotfiles: "allow" };
export default async function buildApp() {
const app = express();
@@ -95,10 +99,10 @@ export default async function buildApp() {
// localhost-only guard and does not require Trilium authentication.
mcpRoutes.register(app);
app.use(express.static(path.join(publicDir, "root")));
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest")));
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
app.use(express.static(path.join(publicDir, "root"), STATIC_OPTIONS));
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest"), STATIC_OPTIONS));
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"), STATIC_OPTIONS));
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"), STATIC_OPTIONS));
const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js");
app.use(sessionParser);

File diff suppressed because one or more lines are too long

View File

@@ -12,12 +12,13 @@
on other Chromium-based browsers as well, but they are not officially supported.</li>
</ul>
<h2>Obtaining the extension</h2>
<aside class="admonition warning">
<p>The extension is currently under development. A preview with unsigned
extensions is available on <a href="https://github.com/TriliumNext/Trilium/actions/runs/21318809414">GitHub Actions</a>.</p>
<p>We have already submitted the extension to both Chrome and Firefox web
stores, but they are pending validation.</p>
</aside>
<p>The extension is available from the official browser web stores:</p>
<ul>
<li><strong>Firefox</strong>: <a href="https://addons.mozilla.org/firefox/addon/trilium-notes-web-clipper/">Trilium Web Clipper on Firefox Add-ons</a>
</li>
<li><strong>Chrome</strong>: <a href="https://chromewebstore.google.com/detail/trilium-web-clipper/ofoiklieachadcaeffficgjaajojpkpi">Trilium Web Clipper on Chrome Web Store</a>
</li>
</ul>
<h2>Functionality</h2>
<ul>
<li>select text and clip it with the right-click context menu</li>

View File

@@ -6,6 +6,7 @@
<img style="aspect-ratio:886/663;" src="2_Mermaid Diagrams_image.png"
width="886" height="663">
</figure>
<h2>Types of diagrams</h2>
<p>Trilium supports Mermaid, which adds support for various diagrams such
as flowchart, sequence diagram, class diagram, state diagram, pie charts,
@@ -48,34 +49,30 @@
<img src="1_Mermaid Diagrams_image.png">
</li>
<li>The preview can be moved around by holding the left mouse button and dragging.</li>
<li
>Zooming can also be done by using the scroll wheel.</li>
<li>The zoom and position on the preview will remain fixed as the diagram
changes, to be able to work more easily with large diagrams.</li>
</ul>
<li>Zooming can also be done by using the scroll wheel.</li>
<li>The zoom and position on the preview will remain fixed as the diagram
changes, to be able to work more easily with large diagrams.</li>
</ul>
</li>
<li>The size of the source/preview panes can be adjusted by hovering over
the border between them and dragging it with the mouse.</li>
<li>In the&nbsp;<a class="reference-link" href="#root/_help_XpOYSgsLkTJy">Floating buttons</a>&nbsp;area:
<ul>
<li>The source/preview can be laid out left-right or bottom-top via the <em>Move editing pane to the left / bottom</em> option.</li>
<li
>Press <em>Lock editing</em> to automatically mark the note as read-only.
<li>Press <em>Lock editing</em> to automatically mark the note as read-only.
In this mode, the code pane is hidden and the diagram is displayed full-size.
Similarly, press <em>Unlock editing</em> to mark a read-only note as editable.</li>
<li
>Press the <em>Copy image reference to the clipboard</em> to be able to insert
the image representation of the diagram into a text note. See&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;for more information.</li>
<li
>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
of the diagram. Can be used to present the diagram without degrading when
zooming.</li>
<li>Press the <em>Copy image reference to the clipboard</em> to be able to insert
the image representation of the diagram into a text note. See&nbsp;<a class="reference-link"
href="#root/_help_0Ofbk1aSuVRu">Image references</a>&nbsp;for more information.</li>
<li>Press the <em>Export diagram as SVG</em> to download a scalable/vector rendering
of the diagram. Can be used to present the diagram without degrading when
zooming.</li>
<li>Press the <em>Export diagram as PNG</em> to download a normal image (at
1x scale, raster) of the diagram. Can be used to send the diagram in more
traditional channels such as e-mail.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2>Errors in the diagram</h2>
<p>If there is an error in the source code, the error will be displayed in

View File

@@ -0,0 +1,86 @@
<p>Trilium supports spell checking for your notes. How it works depends on
whether you're using the <strong>desktop application</strong> (Electron)
or accessing Trilium through a <strong>web browser</strong>.</p>
<h2>Desktop</h2>
<p>The desktop app uses Chromium's built-in spellchecker. You can configure
it from <em>Options</em><strong> </strong><em>Spell Check</em>.</p>
<h3>Enabling spell check</h3>
<p>Toggle <em>Check spelling</em> to enable or disable the spellchecker. A
restart is required for changes to take effect — use the restart button
at the bottom of the section.</p>
<h3>Choosing languages</h3>
<p>When spell check is enabled, a <em>Spell Check Languages</em> section appears
listing all languages available on your system. Select one or more languages
by checking the boxes. The spellchecker will accept words that are valid
in <em>any</em> of the selected languages.</p>
<p>The available languages depend on your operating system's installed language
packs. For example, on Windows you can add languages through <em>Options </em><em>Time &amp; Language </em><em>Language &amp; Region </em><em>Add a language</em>.</p>
<aside
class="admonition note">
<p>The changes take effect only after restarting the application.</p>
</aside>
<h3>Custom dictionary</h3>
<aside class="admonition tip">
<p>This function is available starting with Trilium v0.103.0.</p>
</aside>
<p>Words you add to the dictionary (e.g. via the right-click context menu
→ "Add to dictionary") are stored in a <strong>synced note</strong> inside
Trilium. This means your custom dictionary automatically syncs across all
your devices.</p>
<p>You can view and edit the dictionary directly from <em>Settings </em><em>Spell Check </em><em>Custom Dictionary </em><em>Edit dictionary</em>.
This opens the underlying note, which contains one word per line. You can
add, remove, or modify entries as you like.</p>
<aside class="admonition note">
<p>Changes to the custom dictionary (whether from the editor or the context
menu) take effect after restarting the application.</p>
</aside>
<h4>How the custom dictionary works</h4>
<ul>
<li>When you right-click a misspelled word and choose "Add to dictionary",
the word is saved both to Electron's local spellchecker and to the synced
dictionary note.</li>
<li>On startup, Trilium loads all words from the dictionary note into the
spellchecker session.</li>
<li>If Trilium detects words in Electron's local dictionary but the dictionary
note is empty (e.g. on first use), it performs a <strong>one-time import</strong> of
those words into the note.</li>
<li>Words that are in Electron's local dictionary but <em>not</em> in the note
(e.g. you removed them manually) are cleaned up from the local dictionary
on startup.</li>
</ul>
<h4>Known limitations<a id="known-limitations"></a></h4>
<p>On Windows and macOS, Electron delegates "Add to dictionary" to the operating
system's user dictionary. This means:</p>
<ul>
<li>Words added via the context menu are also written to the OS-level dictionary
(e.g. <code spellcheck="false">%APPDATA%\Microsoft\Spelling\&lt;language&gt;\default.dic</code> on
Windows).</li>
<li><strong>Removing a word</strong> from the Trilium dictionary note prevents
it from being loaded into the spellchecker on next startup, but does <em>not</em> remove
it from the OS dictionary. The word may still be accepted by the OS spellchecker
until you remove it from the OS dictionary manually.</li>
</ul>
<h2>Web browser</h2>
<p>When accessing Trilium through a web browser, spell checking is handled
entirely by the browser itself. Trilium does not control the browser's
spellchecker — language selection, dictionaries, and all other settings
are managed through your browser's preferences.</p>
<p>The Spell Check settings page in Trilium will indicate that these options
apply only to desktop builds.</p>
<h2>Frequently asked questions</h2>
<h3>Do I need to restart after every change?</h3>
<p>Yes. Spell check language selection and the custom dictionary are loaded
once at startup. Any changes require a restart to take effect.</p>
<h3>Can I use multiple spell check languages at the same time?</h3>
<p>Yes. Select as many languages as you need from the checklist. The spellchecker
will accept words from any of the selected languages.</p>
<h3>My custom words disappeared after syncing to a new device — what happened?</h3>
<p>On the first launch of a new device, Trilium may import existing local
dictionary words into the note. If the note already has words from another
device (via sync), those are preserved. Make sure sync completes before
restarting the application on a new device.</p>
<h3>I removed a word from the dictionary note but it's still accepted</h3>
<p>This is likely due to the OS-level dictionary retaining the word (see
<a
href="#known-limitations">Known limitations</a>above). You can manually remove it from your operating
system's user dictionary.</p>

View File

@@ -86,7 +86,24 @@
"copy-without-formatting": "Kopírovat vybraný text bez formátování",
"force-save-revision": "Vynutit vytvoření / uložení nové revize aktivní poznámky",
"export-as-pdf": "Exportovat současnou poznámku jako PDF",
"toggle-zen-mode": "Zapnout/vypnout režim zen (minimalistické uživatelské rozhraní pro soustředěnější úpravy)"
"toggle-zen-mode": "Zapnout/vypnout režim zen (minimalistické uživatelské rozhraní pro soustředěnější úpravy)",
"toggle-basic-properties": "Přepnout základní vlastnosti",
"toggle-file-properties": "Přepnout vlastnosti souboru",
"toggle-image-properties": "Přepnout vlastnosti obrázku",
"toggle-owned-attributes": "Přepnout vlastní atributy",
"toggle-inherited-attributes": "Přepnout zděděné atributy",
"toggle-promoted-attributes": "Přepnout propagované atributy",
"toggle-link-map": "Přepnout mapu odkazů",
"toggle-note-info": "Přepnout informace o poznámce",
"toggle-note-paths": "Přepnout cesty k poznámce",
"toggle-similar-notes": "Přepnout podobné poznámky",
"toggle-right-pane": "Přepnout zobrazení pravého panelu, který obsahuje obsah a zvýraznění",
"toggle-note-hoisting": "Přepnout zúžení zobrazení aktivní poznámky",
"find-in-text": "Přepnout panel hledání",
"toggle-left-note-tree-panel": "Přepnout levý panel (strom poznámek)",
"toggle-full-screen": "Přepnout režim celého obrazovky",
"toggle-book-properties": "Přepnout vlastnosti kolekce",
"toggle-classic-editor-toolbar": "Přepnout záložku formátování pro editor s fixní páskou"
},
"keyboard_action_names": {
"jump-to-note": "Přejít na...",
@@ -107,6 +124,322 @@
"expand-subtree": "Otevřít podstrom",
"collapse-tree": "Zavřít strom",
"collapse-subtree": "Zavřít podstrom",
"sort-child-notes": "Seřadit dceřiné poznámky"
"sort-child-notes": "Seřadit dceřiné poznámky",
"create-note-after": "Vytvořit poznámku po",
"create-note-into": "Vytvořit poznámku do",
"create-note-into-inbox": "Vytvořit poznámku v doručené poště",
"delete-notes": "Smazat poznámky",
"edit-branch-prefix": "Upravit předponu větve",
"paste-notes-from-clipboard": "Vložit poznámky ze schránky",
"cut-notes-to-clipboard": "Vyříznout poznámky do schránky",
"select-all-notes-in-parent": "Vybrat všechny poznámky v nadřazené položce",
"add-note-above-to-selection": "Přidat poznámku nad výběr",
"add-note-below-to-selection": "Přidat poznámku pod výběr",
"duplicate-subtree": "Duplikovat podstrom",
"open-new-tab": "Otevřít novou záložku",
"close-active-tab": "Zavřít aktivní záložku",
"reopen-last-tab": "Znovu otevřít poslední záložku",
"activate-next-tab": "Aktivovat další záložku",
"activate-previous-tab": "Aktivovat předchozí záložku",
"open-new-window": "Otevřít nové okno",
"toggle-system-tray-icon": "Přepínat ikonu v systémové oblasti",
"toggle-zen-mode": "Přepínat režim Zen",
"switch-to-first-tab": "Přepnout na první záložku",
"switch-to-second-tab": "Přepnout na druhou záložku",
"switch-to-third-tab": "Přepnout na třetí záložku",
"switch-to-fourth-tab": "Přepnout na čtvrtou záložku",
"switch-to-fifth-tab": "Přepnout na pátou záložku",
"switch-to-sixth-tab": "Přepnout na šestou záložku",
"switch-to-seventh-tab": "Přepnout na sedmou záložku",
"switch-to-eighth-tab": "Přepnout na osmou záložku",
"switch-to-ninth-tab": "Přepnout na devátou záložku",
"switch-to-last-tab": "Přepnout na poslední záložku",
"show-note-source": "Zobrazit zdroj poznámky",
"show-options": "Zobrazit nastavení",
"show-revisions": "Zobrazit revize",
"show-recent-changes": "Zobrazit nedávné změny",
"show-sql-console": "Zobrazit SQL konzoli",
"show-backend-log": "Zobrazit log backendu",
"show-help": "Zobrazit nápovědu",
"show-cheatsheet": "Zobrazit kısestupku",
"add-link-to-text": "Přidat odkaz do textu",
"follow-link-under-cursor": "Otevřít odkaz pod kurzorem",
"insert-date-and-time-to-text": "Vložit datum a čas do textu",
"paste-markdown-into-text": "Vložit Markdown do textu",
"cut-into-note": "Vyříznout do poznámky",
"add-include-note-to-text": "Přidat zahrnutí poznámky do textu",
"edit-read-only-note": "Upravit poznámku pouze pro čtení",
"add-new-label": "Přidat nový štítek",
"add-new-relation": "Přidat novou vazbu",
"toggle-ribbon-tab-classic-editor": "Přepínat záložku pásu karet Klasický editor",
"toggle-ribbon-tab-basic-properties": "Přepínat záložku pásu karet Základní vlastnosti",
"toggle-ribbon-tab-book-properties": "Přepínat záložku pásu karet Vlastnosti knihy",
"toggle-ribbon-tab-file-properties": "Přepínat záložku pásu karet Vlastnosti souboru",
"toggle-ribbon-tab-image-properties": "Přepínat záložku pásu karet Vlastnosti obrázku",
"toggle-ribbon-tab-owned-attributes": "Přepínat záložku pásu karet Vlastní atributy",
"toggle-ribbon-tab-inherited-attributes": "Přepínat záložku pásu karrét Zděděné atributy",
"toggle-ribbon-tab-promoted-attributes": "Přepínat záložku pásu karet Propagované atributy",
"toggle-ribbon-tab-note-map": "Přepínat záložku pásu karet Mapa poznámky",
"toggle-ribbon-tab-note-info": "Přepínat záložku pásu karet Informace o poznámce",
"toggle-ribbon-tab-note-paths": "Přepínat záložku pásu karet Cesty k poznámce",
"toggle-ribbon-tab-similar-notes": "Přepínat záložku pásu karet Podobné poznámky",
"toggle-right-pane": "Přepnout pravý panel",
"print-active-note": "Tisknout aktivní poznámku",
"export-active-note-as-pdf": "Exportovat aktivní poznámku jako PDF",
"open-note-externally": "Otevřít poznámku externě",
"render-active-note": "Zobrazit aktivní poznámku",
"run-active-note": "Spustit aktivní poznámku",
"toggle-note-hoisting": "Přepnout zúžení zobrazení poznámky",
"unhoist-note": "Zrušit zúžení zobrazení poznámky",
"reload-frontend-app": "Znovu načíst frontend aplikaci",
"open-developer-tools": "Otevřít vývojářské nástroje",
"find-in-text": "Najít v textu",
"toggle-left-pane": "Přepnout levý panel",
"toggle-full-screen": "Přepnout režim celého obrazovky",
"zoom-out": "Zoom out",
"zoom-in": "Zoom in",
"reset-zoom-level": "Resetovat úroveň zvětšení",
"copy-without-formatting": "Kopírovat bez formátování",
"force-save-revision": "Vynutit uložení revize"
},
"login": {
"title": "Přihlášení",
"heading": "Přihlášení do Trilium",
"incorrect-totp": "TOTP je nesprávné. Zkuste to prosím znovu.",
"incorrect-password": "Heslo je nesprávné. Zkuste to prosím znovu.",
"password": "Heslo",
"remember-me": "Zapamatovat si mě",
"button": "Přihlásit se",
"sign_in_with_sso": "Přihlásit se pomocí {{ ssoIssuerName }}"
},
"set_password": {
"title": "Nastavit heslo",
"heading": "Nastavit heslo",
"description": "Než budete moci začít Trilium používat z webu, musíte nejprve nastavit heslo. Toto heslo pak budete používat k přihlášení.",
"password": "Heslo",
"password-confirmation": "Potvrzení hesla",
"button": "Nastavit heslo"
},
"setup": {
"heading": "Nastavení Trilium Notes",
"new-document": "Jsem nový uživatel a chci vytvořit nový dokument Trilium pro své poznámky",
"sync-from-desktop": "Již mám instanci na počítači a chci s ní nastavit synchronizaci",
"sync-from-server": "Již mám instanci na serveru a chci s ní nastavit synchronizaci",
"next": "Další",
"init-in-progress": "Inicializace dokumentu probíhá",
"redirecting": "Budete brzy přesměrováni do aplikace.",
"title": "Nastavení"
},
"setup_sync-from-desktop": {
"heading": "Synchronizace z počítače",
"description": "Toto nastavení musí být zahájeno z instance na počítači:",
"step1": "Otevřete svou instanci Trilium Notes na počítači.",
"step2": "V menu Trilium klikněte na Nastavení.",
"step3": "Klikněte na kategorii Synchronizace.",
"step4": "Změňte adresu instance serveru na: {{- host}} a klikněte na Uložit.",
"step5": "Klikněte na tlačítko „Testovat synchronizaci“ pro ověření úspěšného připojení.",
"step6": "Jakmile tyto kroky dokončíte, klikněte na {{- link}}.",
"step6-here": "zde"
},
"setup_sync-from-server": {
"heading": "Synchronizace ze serveru",
"instructions": "Níže prosím zadejte adresu serveru Trilium a přihlašovací údaje. To stáhne celý dokument Trilium ze serveru a nastaví jeho synchronizaci. V závislosti na velikosti dokumentu a rychlosti vašeho připojení to může trvat nějakou dobu.",
"server-host": "Adresa serveru Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Proxy server (volitelné)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Poznámka:",
"proxy-instruction": "Pokud ponecháte nastavení proxy prázdné, bude použita systémová proxy (platí pouze pro počítačovou aplikaci)",
"password": "Heslo",
"password-placeholder": "Heslo",
"back": "Zpět",
"finish-setup": "Dokončit nastavení"
},
"setup_sync-in-progress": {
"heading": "Synchronizace probíhá",
"successful": "Synchronizace byla správně nastavena. Prvotní synchronizace bude trvat nějaký čas. Jakmile bude hotovo, budete přesměrováni na přihlašovací stránku.",
"outstanding-items": "Neodeslané položky synchronizace:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Nenalezeno",
"heading": "Nenalezeno"
},
"share_page": {
"parent": "nadřazená:",
"clipped-from": "Tato poznámka byla původně uložena ze zdroje {{- url}}",
"child-notes": "Dceřiné poznámky:",
"no-content": "Tato poznámka neobsahuje žádný obsah."
},
"weekdays": {
"monday": "Pondělí",
"tuesday": "Úterý",
"wednesday": "Středa",
"thursday": "Čtvrtek",
"friday": "Pátek",
"saturday": "Sobota",
"sunday": "Neděle"
},
"weekdayNumber": "Týden {{weekNumber}}",
"months": {
"january": "Leden",
"february": "Únor",
"march": "Březen",
"april": "Duben",
"may": "Květen",
"june": "Červen",
"july": "Červenec",
"august": "Srpen",
"september": "Září",
"october": "Říjen",
"november": "Listopad",
"december": "Prosinec"
},
"quarterNumber": "Čtvrtletí {quarterNumber}",
"special_notes": {
"search_prefix": "Hledání:",
"llm_chat_prefix": "Chat:"
},
"test_sync": {
"not-configured": "Hostitel synchronizačního serveru není nakonfigurován. Nejprve prosím nakonfigurujte synchronizaci.",
"successful": "Protokol synchronizačního serveru byl úspěšný, synchronizace byla zahájena."
},
"hidden-subtree": {
"root-title": "Skryté poznámky",
"search-history-title": "Historie hledání",
"note-map-title": "Mapa poznámek",
"sql-console-history-title": "Historie SQL konzole",
"llm-chat-history-title": "Historie AI Chat",
"shared-notes-title": "Sdílené poznámky",
"bulk-action-title": "Hromadná akce",
"backend-log-title": "Log Backend",
"user-hidden-title": "Uživatel skryt",
"launch-bar-templates-title": "Šablony panelu spouštěče",
"base-abstract-launcher-title": "Základní abstraktní spouštěč",
"command-launcher-title": "Spouštěč příkazů",
"note-launcher-title": "Spouštěč poznámky",
"script-launcher-title": "Spouštěč skriptu",
"built-in-widget-title": "Vestavěný widget",
"spacer-title": "Mezera",
"custom-widget-title": "Vlastní widget",
"launch-bar-title": "Panel spouštěče",
"available-launchers-title": "Dostupné spouštěče",
"go-to-previous-note-title": "Přejít na předchozí poznámku",
"go-to-next-note-title": "Přejít na další poznámku",
"new-note-title": "Nová poznámka",
"search-notes-title": "Hledat poznámky",
"jump-to-note-title": "Skočit na...",
"calendar-title": "Kalendář",
"recent-changes-title": "Nedávné změny",
"bookmarks-title": "Záložky",
"command-palette": "Otevřít paletu příkazů",
"zen-mode": "Režim Zen",
"open-today-journal-note-title": "Otevřít dnešní deník",
"quick-search-title": "Rychlé hledání",
"protected-session-title": "Chráněná relace",
"sync-status-title": "Stav synchronizace",
"settings-title": "Nastavení",
"options-title": "Možnosti",
"appearance-title": "Vzhled",
"shortcuts-title": "Zkratky",
"text-notes": "Textové poznámky",
"code-notes-title": "Poznámky s kódem",
"images-title": "Média",
"spellcheck-title": "Kontrola pravopisu",
"password-title": "Heslo",
"multi-factor-authentication-title": "MFA",
"etapi-title": "ETAPI",
"backup-title": "Záloha",
"sync-title": "Synchronizace",
"other": "Ostatní",
"advanced-title": "Pokročilé",
"llm-title": "AI / LLM",
"visible-launchers-title": "Viditelné spouštěče",
"user-guide": "Uživatelská příručka",
"localization": "Jazyk a region",
"inbox-title": "Schránka příchozí",
"tab-switcher-title": "Přepínač záložek",
"sidebar-chat-title": "AI Chat"
},
"notes": {
"new-note": "Nová poznámka",
"duplicate-note-suffix": "(dup)",
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
},
"backend_log": {
"log-does-not-exist": "Logovací soubor Backend '{{ fileName }}' neexistuje (zatím).",
"reading-log-failed": "Čtení logovacího souboru Backend '{{ fileName }}' se nepodařilo."
},
"content_renderer": {
"note-cannot-be-displayed": "Tento typ poznámky nelze zobrazit."
},
"pdf": {
"export_filter": "PDF dokument (*.pdf)",
"unable-to-export-message": "Aktuální poznámku nebylo možné exportovat jako PDF.",
"unable-to-export-title": "Nelze exportovat jako PDF",
"unable-to-save-message": "Do vybraného souboru nebylo možné zapisovat. Zkuste to znovu nebo vyberte jiné cílové místo.",
"unable-to-print": "Nelze vytisknout poznámku"
},
"tray": {
"tooltip": "Trilium Notes",
"close": "Ukončit Trilium",
"recents": "Nedávné poznámky",
"bookmarks": "Záložky (oblíbené)",
"today": "Otevřít dnešní deníkovou poznámku",
"new-note": "Nová poznámka",
"show-windows": "Zobrazit okna",
"open_new_window": "Otevřít nové okno"
},
"migration": {
"old_version": "Přímá migrace z vaší aktuální verze není podporována. Nejprve prosím upgradujte na nejnovější v0.60.4 a až poté na tuto verzi.",
"error_message": "Chyba během migrace na verzi {{version}}: {{stack}}",
"wrong_db_version": "Verze databáze ({{version}}) je novější, než jakou aplikace očekává ({{targetVersion}}), což znamená, že byla vytvořena novější a nekompatibilní verzí Trilium. Pro vyřešení tohoto problému upgradujte na nejnovější verzi Trilium."
},
"modals": {
"error_title": "Chyba"
},
"share_theme": {
"site-theme": "Motiv webu",
"search_placeholder": "Hledat...",
"image_alt": "Obrázek článku",
"last-updated": "Poslední aktualizace dne {{ - date}}",
"subpages": "Podstránky:",
"on-this-page": "Na této stránce",
"expand": "Rozbalit"
},
"hidden_subtree_templates": {
"text-snippet": "Textový úryvek",
"description": "Popis",
"list-view": "Seznamový pohled",
"grid-view": "Mřížkový pohled",
"calendar": "Kalendář",
"table": "Tabulka",
"geo-map": "Geomapa",
"start-date": "Počáteční datum",
"end-date": "Koncové datum",
"start-time": "Počáteční čas",
"end-time": "Koncowy čas",
"geolocation": "Geolokalizace",
"built-in-templates": "Vestavěné šablony",
"board": "Kanbanová tabule",
"status": "Stav",
"board_note_first": "První poznámka",
"board_note_second": "Druhá poznámka",
"board_note_third": "Třetí poznámka",
"board_status_todo": "K dokončení",
"board_status_progress": "Probíhá",
"board_status_done": "Hotovo",
"presentation": "Prezentace",
"presentation_slide": "Prezentace snímku",
"presentation_slide_first": "První snímek",
"presentation_slide_second": "Druhý snímek",
"background": "Pozadí"
},
"sql_init": {
"db_not_initialized_desktop": "DB není inicializována, postupujte podle pokynů na obrazovce.",
"db_not_initialized_server": "DB není inicializována, navštivte prosím stránku pro nastavení - http://[your-server-host]:{{port}}, kde najdete pokyny, jak inicializovat Trilium."
},
"desktop": {
"instance_already_running": "Instance již běží, místo vytváření nové se zaměříme na tu stávající."
}
}

View File

@@ -313,6 +313,7 @@
"shared-notes-title": "Shared Notes",
"bulk-action-title": "Bulk Action",
"backend-log-title": "Backend Log",
"custom-dictionary-title": "Custom Dictionary",
"user-hidden-title": "User Hidden",
"launch-bar-templates-title": "Launch Bar Templates",
"base-abstract-launcher-title": "Base Abstract Launcher",
@@ -405,7 +406,10 @@
"last-updated": "Last updated on {{- date}}",
"subpages": "Subpages:",
"on-this-page": "On This Page",
"expand": "Expand"
"expand": "Expand",
"toggle-navigation": "Toggle Navigation",
"toggle-toc": "Toggle Table of Contents",
"logo-alt": "Logo"
},
"hidden_subtree_templates": {
"text-snippet": "Text Snippet",

View File

@@ -359,7 +359,8 @@
"tab-switcher-title": "Athraitheoir Cluaisíní",
"llm-chat-history-title": "Stair Comhrá AI",
"llm-title": "AI / LLM",
"sidebar-chat-title": "Comhrá AI"
"sidebar-chat-title": "Comhrá AI",
"custom-dictionary-title": "Foclóir Saincheaptha"
},
"notes": {
"new-note": "Nóta nua",
@@ -405,7 +406,10 @@
"last-updated": "Nuashonraithe go deireanach ar {{- date}}",
"subpages": "Fo-leathanaigh:",
"on-this-page": "Ar an Leathanach seo",
"expand": "Leathnaigh"
"expand": "Leathnaigh",
"toggle-navigation": "Nascleanúint a Athrú",
"toggle-toc": "Clár Ábhair a Athsholáthar",
"logo-alt": "Lógó"
},
"hidden_subtree_templates": {
"text-snippet": "Sleachta Téacs",

View File

@@ -102,7 +102,7 @@
"shortcuts-title": "Scorciatoie",
"text-notes": "Note di testo",
"code-notes-title": "Note di codice",
"images-title": "Immagini",
"images-title": "Media",
"spellcheck-title": "Controllo ortografico",
"password-title": "Password",
"multi-factor-authentication-title": "Autenticazione a più fattori",
@@ -151,7 +151,8 @@
"tab-switcher-title": "Selettore scheda",
"llm-chat-history-title": "Cronologia chat IA",
"llm-title": "AI / LLM",
"sidebar-chat-title": "Chat con IA"
"sidebar-chat-title": "Chat con IA",
"custom-dictionary-title": "Dizionario personalizzato"
},
"notes": {
"new-note": "Nuova nota",
@@ -197,7 +198,10 @@
"last-updated": "Ultimo aggiornamento il {{- date}}",
"subpages": "Sottopagine:",
"on-this-page": "In questa pagina",
"expand": "Espandi"
"expand": "Espandi",
"toggle-navigation": "Attiva/disattiva la navigazione",
"toggle-toc": "Mostra/Nascondi sommario",
"logo-alt": "Logo"
},
"keyboard_actions": {
"back-in-note-history": "Naviga alla nota precedente della cronologia",

View File

@@ -347,7 +347,8 @@
"tab-switcher-title": "タブ切り替え",
"llm-chat-history-title": "AI チャット履歴",
"llm-title": "AI / LLM",
"sidebar-chat-title": "AI チャット"
"sidebar-chat-title": "AI チャット",
"custom-dictionary-title": "カスタム辞書"
},
"notes": {
"new-note": "新しいノート",
@@ -393,7 +394,10 @@
"subpages": "サブページ:",
"image_alt": "記事画像",
"on-this-page": "このページの内容",
"expand": "展開"
"expand": "展開",
"toggle-navigation": "ナビゲーションの切り替え",
"toggle-toc": "目次の切り替え",
"logo-alt": "ロゴ"
},
"hidden_subtree_templates": {
"text-snippet": "テキストスニペット",

View File

@@ -19,6 +19,16 @@
"move-note-down": "Notu aşağıya kaydır",
"create-note-into-inbox": "Eğer tanımlandıysa gelen kutusunda bir not veya günlük not oluşturun",
"move-note-up-in-hierarchy": "Notu hiyerarşide yukarı taşı",
"move-note-down-in-hierarchy": "Notu hiyerarşide aşağı taşı"
"move-note-down-in-hierarchy": "Notu hiyerarşide aşağı taşı",
"edit-note-title": "Ağaç yapısından not detayına atla ve başlığı düzenle",
"edit-branch-prefix": "\"Dal önekini düzenle\" iletişim kutusunu göster",
"clone-notes-to": "Seçilen notları klonla",
"move-notes-to": "Seçilen notları taşı",
"note-clipboard": "Not panosu",
"copy-notes-to-clipboard": "Seçilmiş notları panoya kopyala",
"paste-notes-from-clipboard": "Panodan notları etkin nota yapıştırın",
"cut-notes-to-clipboard": "Seçilen notları panoya kes",
"select-all-notes-in-parent": "Geçerli nota seviyesinden tüm notaları seçin",
"add-note-above-to-the-selection": "Seçime yukarıdaki notu ekleyin"
}
}

View File

@@ -198,7 +198,8 @@
"december": "十二月"
},
"special_notes": {
"search_prefix": "搜尋:"
"search_prefix": "搜尋:",
"llm_chat_prefix": "對話:"
},
"test_sync": {
"not-configured": "尚未設定同步伺服器主機,請先設定同步。",
@@ -340,7 +341,7 @@
"shortcuts-title": "快捷鍵",
"text-notes": "文字筆記",
"code-notes-title": "程式碼筆記",
"images-title": "圖片",
"images-title": "媒體",
"spellcheck-title": "拼寫檢查",
"password-title": "密碼",
"multi-factor-authentication-title": "多重身份驗證",
@@ -355,7 +356,10 @@
"inbox-title": "收件匣",
"command-palette": "打開命令面板",
"zen-mode": "禪模式",
"tab-switcher-title": "切換分頁"
"tab-switcher-title": "切換分頁",
"llm-chat-history-title": "AI 對話歷史",
"llm-title": "AI / LLM",
"sidebar-chat-title": "AI 對話"
},
"notes": {
"new-note": "新增筆記",

View File

@@ -12,8 +12,8 @@
display: none;
}
</style>
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next-light.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next-dark.css" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
</head>

View File

@@ -6,8 +6,8 @@
<title><%= t("set_password.title") %></title>
<link rel="apple-touch-icon" sizes="180x180" href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png">
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next-light.css" media="(prefers-color-scheme: light)">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next-dark.css" media="(prefers-color-scheme: dark)">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
<style>

View File

@@ -171,8 +171,8 @@
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="<%= appPath %>/setup.js" crossorigin type="module"></script>
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet" />
<link href="<%= assetPath %>/stylesheets/theme-next.css" rel="stylesheet" />
<link href="<%= assetPath %>/stylesheets/theme-next-light.css" rel="stylesheet" media="(prefers-color-scheme: light)" />
<link href="<%= assetPath %>/stylesheets/theme-next-dark.css" rel="stylesheet" media="(prefers-color-scheme: dark)" />
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
</body>
</html>

View File

@@ -316,6 +316,18 @@ class BNote extends AbstractBeccaEntity<BNote> {
return null;
}
/**
* Executes this note as a script. The note must be of type "Code: JS backend".
*
* @returns the return value of the executed script
*/
executeScript() {
// Lazy require to avoid circular dependency (script.ts imports BNote as a type).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const scriptService = require("../../services/script.js").default;
return scriptService.executeNote(this, { originEntity: this });
}
/**
* Beware that the method must not create a copy of the array, but actually returns its internal array
* (for performance reasons)

View File

@@ -11,7 +11,8 @@ const MIGRATIONS: (SqlMigration | JsMigration)[] = [
version: 236,
sql: /*sql*/`\
ALTER TABLE blobs ADD COLUMN textRepresentation TEXT DEFAULT NULL;
`
`,
ignoreErrors: true
},
// Add missing database indices for query performance
{
@@ -335,6 +336,8 @@ export default MIGRATIONS;
interface Migration {
version: number;
/** If true, errors during this migration are logged but do not halt the migration process. Useful for migrations that may have already been applied (e.g. adding a column that already exists). */
ignoreErrors?: boolean;
}
interface SqlMigration extends Migration {

View File

@@ -5,7 +5,7 @@ import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import config from "../../services/config.js";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import { changeLanguage } from "../../services/i18n.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import searchService from "../../services/search/services/search.js";
@@ -192,10 +192,6 @@ function getUserThemes() {
return ret;
}
function getSupportedLocales() {
return getLocales();
}
function isAllowed(name: string) {
return (ALLOWED_OPTIONS as Set<string>).has(name)
|| name.startsWith("keyboardShortcuts")
@@ -207,6 +203,5 @@ export default {
getOptions,
updateOption,
updateOptions,
getUserThemes,
getSupportedLocales
getUserThemes
};

View File

@@ -9,6 +9,9 @@ import auth from "../services/auth.js";
import { getResourceDir, isDev } from "../services/utils.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
// Allow serving assets even if the installation path contains a hidden (dot-prefixed) directory.
const STATIC_OPTIONS: serveStatic.ServeStaticOptions = { dotfiles: "allow" };
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
if (!isDev) {
options = {
@@ -16,7 +19,7 @@ const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOp
...options
};
}
return express.static(root, options);
return express.static(root, { ...STATIC_OPTIONS, ...options });
};
async function register(app: express.Application) {
@@ -66,7 +69,7 @@ async function register(app: express.Application) {
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(publicDir, "index.html"));
res.sendFile(path.join(publicDir, "index.html"), STATIC_OPTIONS);
});
app.use("/assets", persistentCacheStatic(path.join(publicDir, "assets")));
app.use(`/src`, persistentCacheStatic(path.join(publicDir, "src")));
@@ -76,14 +79,14 @@ async function register(app: express.Application) {
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(publicDir, "translations")));
app.use(`/node_modules/`, persistentCacheStatic(path.join(publicDir, "node_modules")));
}
app.use(`/share/assets/fonts/`, express.static(path.join(getClientDir(), "fonts")));
app.use(`/share/assets/`, express.static(getShareThemeAssetDir()));
app.use(`/share/assets/fonts/`, express.static(path.join(getClientDir(), "fonts"), STATIC_OPTIONS));
app.use(`/share/assets/`, express.static(getShareThemeAssetDir(), STATIC_OPTIONS));
app.use(`/pdfjs/`, persistentCacheStatic(getPdfjsAssetDir()));
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts")));
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images")));
app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets")));
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts"), STATIC_OPTIONS));
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images"), STATIC_OPTIONS));
app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets"), STATIC_OPTIONS));
}
export function getShareThemeAssetDir() {

View File

@@ -11,9 +11,9 @@ import { generateCss, generateIconRegistry, getIconPacks, MIME_TO_EXTENSION_MAPP
import log from "../services/log.js";
import optionService from "../services/options.js";
import protectedSessionService from "../services/protected_session.js";
import { generateCsrfToken } from "./csrf_protection.js";
import sql from "../services/sql.js";
import { isDev, isElectron, isMac, isWindows11 } from "../services/utils.js";
import { generateCsrfToken } from "./csrf_protection.js";
type View = "desktop" | "mobile" | "print";
@@ -38,6 +38,7 @@ export function bootstrap(req: Request, res: Response) {
const view = getView(req);
const theme = options.theme;
const themeNote = attributeService.getNoteWithLabel("appTheme", theme);
const themeUseNextAsBase = themeNote?.getAttributeValue("label", "appThemeBase") ?? undefined;
const nativeTitleBarVisible = options.nativeTitleBarVisible === "true";
const iconPacks = getIconPacks();
const currentLocale = getCurrentLocale();
@@ -45,8 +46,9 @@ export function bootstrap(req: Request, res: Response) {
res.send({
device: view,
csrfToken,
themeCssUrl: getThemeCssUrl(theme, themeNote),
themeUseNextAsBase: themeNote?.getAttributeValue("label", "appThemeBase"),
theme,
themeBase: themeUseNextAsBase,
customThemeCssUrl: getCustomThemeCssUrl(theme, themeNote),
headingStyle: options.headingStyle,
layoutOrientation: options.layoutOrientation,
platform: process.platform,
@@ -117,25 +119,16 @@ function getView(req: Request): View {
return "desktop";
}
function getThemeCssUrl(theme: string, themeNote: BNote | null) {
if (theme === "auto") {
return `${assetPath}/stylesheets/theme.css`;
} else if (theme === "light") {
// light theme is always loaded as baseline
return false;
} else if (theme === "dark") {
return `${assetPath}/stylesheets/theme-dark.css`;
} else if (theme === "next") {
return `${assetPath}/stylesheets/theme-next.css`;
} else if (theme === "next-light") {
return `${assetPath}/stylesheets/theme-next-light.css`;
} else if (theme === "next-dark") {
return `${assetPath}/stylesheets/theme-next-dark.css`;
} else if (!process.env.TRILIUM_SAFE_MODE && themeNote) {
function getCustomThemeCssUrl(theme: string, themeNote: BNote | null) {
if (["auto", "light", "dark", "next", "next-light", "next-dark"].includes(theme)) {
return undefined;
}
if (!process.env.TRILIUM_SAFE_MODE && themeNote) {
return `api/notes/download/${themeNote.noteId}`;
}
// baseline light theme
return false;
return undefined;
}
function getAppCssNoteIds() {

View File

@@ -215,7 +215,6 @@ function register(app: express.Application) {
apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);

View File

@@ -1,41 +1,42 @@
import log from "./log.js";
import noteService from "./notes.js";
import sql from "./sql.js";
import { randomString, escapeHtml, unescapeHtml } from "./utils.js";
import attributeService from "./attributes.js";
import dateNoteService from "./date_notes.js";
import treeService from "./tree.js";
import config from "./config.js";
import axios from "axios";
import type { AttributeRow } from "@triliumnext/commons";
import { dayjs } from "@triliumnext/commons";
import xml2js from "xml2js";
import { formatLogMessage } from "@triliumnext/commons";
import axios from "axios";
import * as cheerio from "cheerio";
import cloningService from "./cloning.js";
import appInfo from "./app_info.js";
import searchService from "./search/services/search.js";
import SearchContext from "./search/search_context.js";
import xml2js from "xml2js";
import becca from "../becca/becca.js";
import ws from "./ws.js";
import type Becca from "../becca/becca-interface.js";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BNote from "../becca/entities/bnote.js";
import type BOption from "../becca/entities/boption.js";
import type BRevision from "../becca/entities/brevision.js";
import appInfo from "./app_info.js";
import attributeService from "./attributes.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import backupService from "./backup.js";
import branchService from "./branches.js";
import cloningService from "./cloning.js";
import config from "./config.js";
import dateNoteService from "./date_notes.js";
import exportService from "./export/zip.js";
import log from "./log.js";
import type { NoteParams } from "./note-interface.js";
import noteService from "./notes.js";
import optionsService from "./options.js";
import SearchContext from "./search/search_context.js";
import searchService from "./search/services/search.js";
import SpacedUpdate from "./spaced_update.js";
import specialNotesService from "./special_notes.js";
import branchService from "./branches.js";
import exportService from "./export/zip.js";
import sql from "./sql.js";
import syncMutex from "./sync_mutex.js";
import backupService from "./backup.js";
import optionsService from "./options.js";
import { formatLogMessage } from "@triliumnext/commons";
import type BNote from "../becca/entities/bnote.js";
import type AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BAttachment from "../becca/entities/battachment.js";
import type BRevision from "../becca/entities/brevision.js";
import type BEtapiToken from "../becca/entities/betapi_token.js";
import type BOption from "../becca/entities/boption.js";
import type { AttributeRow } from "@triliumnext/commons";
import type Becca from "../becca/becca-interface.js";
import type { NoteParams } from "./note-interface.js";
import type { ApiParams } from "./backend_script_api_interface.js";
import treeService from "./tree.js";
import { escapeHtml, randomString, unescapeHtml } from "./utils.js";
import ws from "./ws.js";
/**
* A whole number
@@ -506,7 +507,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
throw new Error(`Unable to find parent note with ID ${parentNote}.`);
}
let extraOptions: NoteParams = {
const extraOptions: NoteParams = {
..._extraOptions,
content: "",
type: "text",
@@ -620,13 +621,13 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
}
const parentNoteId = opts.isVisible ? "_lbVisibleLaunchers" : "_lbAvailableLaunchers";
const noteId = "al_" + opts.id;
const noteId = `al_${opts.id}`;
const launcherNote =
becca.getNote(noteId) ||
specialNotesService.createLauncher({
noteId: noteId,
parentNoteId: parentNoteId,
noteId,
parentNoteId,
launcherType: opts.type
}).note;
@@ -680,7 +681,7 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
ws.sendMessageToAllClients({
type: "execute-script",
script: script,
script,
params: prepareParams(params),
startNoteId: this.startNote?.noteId,
currentNoteId: this.currentNote.noteId,
@@ -696,9 +697,8 @@ function BackendScriptApi(this: Api, currentNote: BNote, apiParams: ApiParams) {
return params.map((p) => {
if (typeof p === "function") {
return `!@#Function: ${p.toString()}`;
} else {
return p;
}
}
return p;
});
}
};

View File

@@ -0,0 +1,171 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import becca from "../becca/becca.js";
import { buildNote } from "../test/becca_easy_mocking.js";
import customDictionary from "./custom_dictionary.js";
vi.mock("./log.js", () => ({
default: {
info: vi.fn(),
error: vi.fn()
}
}));
vi.mock("./sql.js", () => ({
default: {
transactional: (cb: Function) => cb(),
execute: () => {},
replace: () => {},
getMap: () => {},
getValue: () => null,
upsert: () => {}
}
}));
function mockSession(localWords: string[] = []) {
return {
listWordsInSpellCheckerDictionary: vi.fn().mockResolvedValue(localWords),
addWordToSpellCheckerDictionary: vi.fn(),
removeWordFromSpellCheckerDictionary: vi.fn()
} as any;
}
describe("custom_dictionary", () => {
beforeEach(() => {
vi.clearAllMocks();
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: ""
});
});
describe("loadForSession", () => {
it("does nothing when note is empty and no local words", async () => {
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("imports local words when note is empty (one-time import)", async () => {
const session = mockSession(["hello", "world"]);
await customDictionary.loadForSession(session);
// Words are saved to the note; they're already in the local dictionary so no re-add needed.
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("does not remove or re-add local words after one-time import", async () => {
const session = mockSession(["hello", "world"]);
await customDictionary.loadForSession(session);
// Words were imported from local, so they already exist — no remove, no re-add.
expect(session.removeWordFromSpellCheckerDictionary).not.toHaveBeenCalled();
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
it("loads note words into session when no local words exist", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
});
it("only adds note words not already in local dictionary", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "banana" is already local, so only "apple" needs adding.
const session = mockSession(["banana", "cherry"]);
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
});
it("only removes local words not in the note", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "cherry" is not in the note, so it should be removed. "banana" should stay.
const session = mockSession(["banana", "cherry"]);
await customDictionary.loadForSession(session);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
});
it("handles note with whitespace and blank lines", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: " apple \n\n banana \n\n"
});
const session = mockSession();
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledTimes(2);
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("apple");
expect(session.addWordToSpellCheckerDictionary).toHaveBeenCalledWith("banana");
});
it("does not re-add words removed from the note but present locally", async () => {
becca.reset();
buildNote({
id: "_customDictionary",
title: "Custom Dictionary",
type: "code",
content: "apple\nbanana"
});
// "cherry" was previously in the note but user removed it;
// it still lingers in Electron's local dictionary.
const session = mockSession(["apple", "banana", "cherry"]);
await customDictionary.loadForSession(session);
// "apple" and "banana" are already local — no re-add needed.
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
// "cherry" should be removed from local dictionary.
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledTimes(1);
expect(session.removeWordFromSpellCheckerDictionary).toHaveBeenCalledWith("cherry");
});
it("handles missing dictionary note gracefully", async () => {
becca.reset(); // no note created
const session = mockSession(["hello"]);
await customDictionary.loadForSession(session);
expect(session.addWordToSpellCheckerDictionary).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,113 @@
import type { Session } from "electron";
import becca from "../becca/becca.js";
import cls from "./cls.js";
import log from "./log.js";
const DICTIONARY_NOTE_ID = "_customDictionary";
/**
* Reads the custom dictionary words from the hidden note.
*/
function getWords(): Set<string> {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
return new Set();
}
const content = note.getContent();
if (typeof content !== "string" || !content.trim()) {
return new Set();
}
return new Set(
content.split("\n")
.map((w) => w.trim())
.filter((w) => w.length > 0)
);
}
/**
* Saves the given words to the custom dictionary note, one per line.
*/
function saveWords(words: Set<string>) {
cls.init(() => {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
log.error("Custom dictionary note not found.");
return;
}
const sorted = [...words].sort((a, b) => a.localeCompare(b));
note.setContent(sorted.join("\n"));
});
}
/**
* Adds a single word to the custom dictionary note.
*/
function addWord(word: string) {
const words = getWords();
words.add(word);
saveWords(words);
}
/**
* Removes all words from Electron's local spellchecker dictionary
* so they are not re-imported on subsequent startups.
*/
function clearFromLocalDictionary(session: Session, localWords: string[]) {
for (const word of localWords) {
session.removeWordFromSpellCheckerDictionary(word);
}
log.info(`Cleared ${localWords.length} words from local spellchecker dictionary.`);
}
/**
* Loads the custom dictionary into Electron's spellchecker session,
* performing a one-time import of locally stored words on first use.
*/
async function loadForSession(session: Session) {
const note = becca.getNote(DICTIONARY_NOTE_ID);
if (!note) {
log.error("Custom dictionary note not found.");
return;
}
const noteWords = getWords();
const localWords = await session.listWordsInSpellCheckerDictionary();
let merged = noteWords;
// One-time import: if the note is empty but there are local words, import them.
if (noteWords.size === 0 && localWords.length > 0) {
log.info(`Importing ${localWords.length} words from local spellchecker dictionary.`);
merged = new Set(localWords);
saveWords(merged);
}
// Remove local words that are not in the note (e.g. user removed them manually).
const staleWords = localWords.filter((w) => !merged.has(w));
if (staleWords.length > 0) {
clearFromLocalDictionary(session, staleWords);
}
// Add note words that aren't already in the local dictionary.
const localWordsSet = new Set(localWords);
for (const word of merged) {
if (!localWordsSet.has(word)) {
session.addWordToSpellCheckerDictionary(word);
}
}
if (merged.size > 0) {
log.info(`Loaded ${merged.size} custom dictionary words into spellchecker.`);
}
}
export default {
getWords,
saveWords,
addWord,
loadForSession
};

View File

@@ -93,6 +93,12 @@ function buildHiddenSubtreeDefinition(helpSubtree: HiddenSubtreeItem[]): HiddenS
{ type: "label", name: "fullContentWidth" }
]
},
{
id: "_customDictionary",
title: t("hidden-subtree.custom-dictionary-title"),
type: "code",
icon: "bx-book"
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: "_userHidden",

View File

@@ -4,7 +4,7 @@ import sql_init from "./sql_init.js";
import { join } from "path";
import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js";
import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type LOCALE_IDS } from "@triliumnext/commons";
export async function initializeTranslations() {
const resourceDir = getResourceDir();
@@ -30,10 +30,6 @@ export function ordinal(date: Dayjs) {
.format("Do");
}
export function getLocales(): Locale[] {
return LOCALES;
}
function getCurrentLanguage(): LOCALE_IDS {
let language: string | null = null;
if (sql_init.isDbInitialized()) {

View File

@@ -71,6 +71,11 @@ describe("processNoteContent", () => {
expect(content).toContain(`<img src="api/images/${bananaNote!.noteId}/banana.jpeg`);
});
it("can import ZIP with UTF-8 filenames without language encoding flag", async () => {
const { importedNote } = await testImport("utf8-filename.zip");
expect(importedNote.title).toBe("测试");
});
it("can import old geomap notes", async () => {
const { importedNote } = await testImport("geomap.zip");
expect(importedNote.type).toBe("book");

View File

@@ -659,13 +659,20 @@ export function readContent(zipfile: yauzl.ZipFile, entry: yauzl.Entry): Promise
export function readZipFile(buffer: Buffer, processEntryCallback: (zipfile: yauzl.ZipFile, entry: yauzl.Entry) => Promise<void>) {
return new Promise<void>((res, rej) => {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false }, (err, zipfile) => {
yauzl.fromBuffer(buffer, { lazyEntries: true, validateEntrySizes: false, decodeStrings: false }, (err, zipfile) => {
if (err) rej(err);
if (!zipfile) throw new Error("Unable to read zip file.");
zipfile.readEntry();
zipfile.on("entry", async (entry) => {
try {
// yauzl with decodeStrings: false returns fileName as a Buffer.
// We decode as UTF-8 to handle ZIP files that use UTF-8 filenames
// without setting the general purpose bit flag 11 (language encoding flag).
if (Buffer.isBuffer(entry.fileName)) {
entry.fileName = (entry.fileName as Buffer).toString("utf-8");
}
await processEntryCallback(zipfile, entry);
} catch (e) {
rej(e);

View File

@@ -8,9 +8,7 @@ import dataDir from "./data_dir.js";
import cls from "./cls.js";
import config, { LOGGING_DEFAULT_RETENTION_DAYS } from "./config.js";
if (!fs.existsSync(dataDir.LOG_DIR)) {
fs.mkdirSync(dataDir.LOG_DIR, 0o700);
}
fs.mkdirSync(dataDir.LOG_DIR, { recursive: true, mode: 0o700 });
let logFile: fs.WriteStream | undefined;

View File

@@ -14,6 +14,7 @@ interface MigrationInfo {
* If a function, then the migration is a JavaScript/TypeScript module that will be executed.
*/
migration: string | (() => void);
ignoreErrors?: boolean;
}
async function migrate() {
@@ -56,9 +57,13 @@ async function migrate() {
log.info(`Migration to version ${mig.dbVersion} has been successful.`);
} catch (e: any) {
console.error(e);
crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
break; // crash() is sometimes async
if (mig.ignoreErrors) {
log.info(`Migration to version ${mig.dbVersion} failed, but ignoreErrors is set. Continuing. Error: ${e.message}`);
} else {
console.error(e);
crash(t("migration.error_message", { version: mig.dbVersion, stack: e.stack }));
break; // crash() is sometimes async
}
}
}
});
@@ -79,14 +84,16 @@ async function prepareMigrations(currentDbVersion: number): Promise<MigrationInf
if ("sql" in migration) {
migrations.push({
dbVersion,
migration: migration.sql
migration: migration.sql,
ignoreErrors: migration.ignoreErrors
});
} else {
// Due to ESM imports, the migration file needs to be imported asynchronously and thus cannot be loaded at migration time (since migration is not asynchronous).
// As such we have to preload the ESM.
migrations.push({
dbVersion,
migration: (await migration.module()).default
migration: (await migration.module()).default,
ignoreErrors: migration.ignoreErrors
});
}
}

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