Compare commits

..

395 Commits

Author SHA1 Message Date
Elian Doran
3101e01478 refactor(desktop-standalone): rename project 2026-03-27 09:27:38 +02:00
Elian Doran
c00a181645 Merge remote-tracking branch 'origin/standalone' into feature/standalone_neutralino 2026-03-27 09:16:58 +02:00
Elian Doran
4c933669b9 Standalone extra improvements (#9191) 2026-03-27 09:15:03 +02:00
Elian Doran
a7001beced chore(standalone): addres requested changes 2026-03-27 09:04:21 +02:00
Elian Doran
b864c338dd chore(standalone): align deps with client 2026-03-27 08:57:51 +02:00
Elian Doran
61d37c4c19 Merge remote-tracking branch 'origin/main' into feature/standalone_extra_improvements 2026-03-27 08:52:25 +02:00
Elian Doran
296579fa87 test(server): initialize core 2026-03-27 00:06:22 +02:00
Elian Doran
995f39dfdf Revert "chore(core): set up basic vitest"
This reverts commit c7cf8d5255.
2026-03-27 00:02:43 +02:00
Elian Doran
c7cf8d5255 chore(core): set up basic vitest 2026-03-26 23:38:13 +02:00
Elian Doran
e1079f954e chore(core): fix one more type error 2026-03-26 23:26:14 +02:00
Elian Doran
d2524adcd2 fix(server): wrong use of isElectron 2026-03-26 23:23:22 +02:00
Elian Doran
e778942711 fix(server): custom route depending on helper function 2026-03-26 23:22:54 +02:00
Elian Doran
04136cd9c0 chore(desktop): strange cannot write file because it would overwrite input file 2026-03-26 23:22:16 +02:00
Elian Doran
247108f347 fix(core): desktop crashing due to missing platform check 2026-03-26 23:09:42 +02:00
Elian Doran
1a8075e2f1 fix(server): server-side translations missing 2026-03-26 22:07:14 +02:00
Elian Doran
b47ede7772 Merge remote-tracking branch 'origin/main' into feature/standalone_extra_improvements 2026-03-26 22:00:05 +02:00
Elian Doran
ebbb8b396c fix(standalone): unable to switch themes 2026-03-26 21:58:28 +02:00
Elian Doran
a2cace6c0f feat(standalone): add support for environment variables 2026-03-26 21:52:58 +02:00
Elian Doran
c0593707f2 refactor(core): use own path replacement 2026-03-26 21:41:11 +02:00
Elian Doran
8b98fdcba1 feat(standalone): support app CSS 2026-03-26 21:37:35 +02:00
Elian Doran
a05c5821b3 chore: fix the rest of the type errors 2026-03-26 21:19:12 +02:00
Elian Doran
140fbc1524 chore: fix various type errors 2026-03-26 21:15:37 +02:00
Elian Doran
6bb093e6d3 chore(client): fix a few type errors 2026-03-26 21:09:04 +02:00
Elian Doran
609ec19e06 chore(edit-docs): fix missing references to core 2026-03-26 21:03:21 +02:00
Elian Doran
acb3030d56 chore(core): fix most bootstrap-related type errors 2026-03-26 20:57:04 +02:00
Elian Doran
0fc5b2e997 chore(core): fix various type errors 2026-03-26 20:35:45 +02:00
Elian Doran
41a7d6738b chore(core): introduce becca_easy_mocking and becca_mocking 2026-03-26 20:24:44 +02:00
Elian Doran
11461221ba chore: solve a few more type errors 2026-03-26 20:15:20 +02:00
Elian Doran
ce25bd10ff chore(core): fix meta types 2026-03-26 20:12:24 +02:00
Elian Doran
9c5bac5741 refactor(core): integrate more utils into core 2026-03-26 19:58:29 +02:00
Elian Doran
9a42536205 chore(core): fix various type errors 2026-03-26 19:58:11 +02:00
Elian Doran
74e0ab071c chore(desktop): forge type config interfering 2026-03-26 19:39:51 +02:00
Elian Doran
0b136f3aae chore(client): typecheck issues due to change in bootstrap definition 2026-03-26 19:38:27 +02:00
Elian Doran
01dae831a4 chore(scripts): improve typecheck with numbers & total count 2026-03-26 19:35:49 +02:00
Elian Doran
e2062558b7 chore(core): typecheck issues due to TypeScript module setting 2026-03-26 19:32:36 +02:00
Elian Doran
259405d707 chore(core): fix typechecks regarding SQL 2026-03-26 19:27:52 +02:00
Elian Doran
ef7502be34 chore(scripts): filter typecheck to avoid cascading errors 2026-03-26 19:27:25 +02:00
Elian Doran
13e26c5b3f chore(core): remove redundant log 2026-03-26 19:16:23 +02:00
Elian Doran
5fec715e3f chore(core): integrate the rest of the note map route 2026-03-26 19:16:12 +02:00
Elian Doran
97443c0682 chore(llm): mention main project distinction 2026-03-26 19:07:00 +02:00
Elian Doran
53c0b920e2 chore(llm): re-init CLAUDE.md 2026-03-26 19:05:36 +02:00
Elian Doran
79b2bc8b93 Standalone setup (#9180) 2026-03-26 19:00:09 +02:00
Elian Doran
360d9d5202 fix(desktop/setup): window with no traffic lights or draggable on macOS 2026-03-26 18:52:06 +02:00
Elian Doran
bf7af98739 fix(client): runtime error due to missing entry 2026-03-26 18:51:49 +02:00
Elian Doran
b574237dfb feat(setup): add a nice banner when DB not initialized 2026-03-26 18:24:36 +02:00
Elian Doran
afe597c811 feat(core): unified crash system using platform provider 2026-03-26 18:17:24 +02:00
Elian Doran
48219f54fc chore(server): remove old translations 2026-03-26 15:25:30 +02:00
Elian Doran
d171409301 chore(setup): remove old files 2026-03-26 15:18:15 +02:00
Elian Doran
e508a4cd43 feat(setup): functional sync from desktop with automatic status update 2026-03-26 14:41:26 +02:00
Elian Doran
a5da35b7ae fix(setup): redirects to /setup on browser 2026-03-26 11:45:52 +02:00
Elian Doran
2016c97a12 chore(scripts): add a way to wipe node modules 2026-03-26 11:27:46 +02:00
Elian Doran
9595f52a9c chore(core): address requested changes 2026-03-26 10:39:29 +02:00
Elian Doran
9ee17445a5 fix(desktop/setup): not finishing setup properly 2026-03-26 00:23:41 +02:00
Elian Doran
cd97e2c861 feat(desktop/setup): add background effects 2026-03-26 00:13:03 +02:00
Elian Doran
db6f034cb5 feat(setup): display network addresses on browser as well 2026-03-25 23:59:52 +02:00
Elian Doran
46b478ec17 feat(desktop/setup): improve waiting display 2026-03-25 23:55:38 +02:00
Elian Doran
de57a39df6 feat(desktop/setup): improve addresses display 2026-03-25 23:45:47 +02:00
Elian Doran
8eb45e2814 feat(desktop/setup): display port in desktop-to-desktop sync 2026-03-25 23:40:40 +02:00
Elian Doran
5bb0887d8b fix(desktop/setup): misleading IP in desktop-to-desktop sync 2026-03-25 23:25:43 +02:00
Elian Doran
b5f7f89c27 feat(desktop/setup): improve sync illustration 2026-03-25 23:15:11 +02:00
Elian Doran
fa7d1d3f80 feat(desktop): improve integration of setup 2026-03-25 23:09:26 +02:00
Elian Doran
2eef2f801f chore(core): don't log language option not found if DB not initialized 2026-03-25 22:55:59 +02:00
Elian Doran
6ebf9f59a0 fix(server): translations not working 2026-03-25 22:53:12 +02:00
Elian Doran
eddb47c9c4 chore(core): bring back SQL initialization with message 2026-03-25 22:48:15 +02:00
Elian Doran
8d38b818c0 feat(core): reintroduce DB migration 2026-03-25 22:16:07 +02:00
Elian Doran
af462ab0f9 chore(standalone/setup): basic mobile support 2026-03-25 22:02:18 +02:00
Elian Doran
07753a6253 refactor(standalone/setup): get rid of warnings 2026-03-25 21:32:55 +02:00
Elian Doran
54b12cf560 chore(standalone/setup): add autocomplete attributes to sync setup 2026-03-25 21:32:05 +02:00
Elian Doran
f97f5da837 fix(standalone/setup): sync from desktop button no longer working 2026-03-25 21:29:41 +02:00
Elian Doran
19e315dc1a fix(server): crash due to session cleanup with unitialized DB 2026-03-25 21:27:35 +02:00
Elian Doran
96d01d6379 i18n(client): minor change 2026-03-25 21:26:10 +02:00
Elian Doran
ee156f1183 fix(server): random error due to font loading while not initialized 2026-03-25 21:26:03 +02:00
Elian Doran
f83e184fcd fix(standalone/setup): current language not restored when going back 2026-03-25 21:14:23 +02:00
Elian Doran
a2ead45c83 style(standalone/setup): make language selection slightly narrower 2026-03-25 21:13:49 +02:00
Elian Doran
b295f1e957 chore(standalone/setup): increase size of setup dialog 2026-03-25 21:11:04 +02:00
Elian Doran
cbd4fd3820 i18n(client): translate setup into Romanian 2026-03-25 21:10:18 +02:00
Elian Doran
b27fa2a555 chore(standalone/setup): set up navigation 2026-03-25 21:10:10 +02:00
Elian Doran
2afd9b474c fix(server): trying to connect to web socket while in setup 2026-03-25 20:56:14 +02:00
Elian Doran
680ac80526 feat(standalone/setup): start working on language selection page 2026-03-25 20:56:01 +02:00
Elian Doran
4b08a33307 feat(standalone/setup): add icon on first page 2026-03-25 20:30:04 +02:00
Elian Doran
04db52145d feat(standalone/setup): use segmented cards for sync setup 2026-03-25 20:23:57 +02:00
Elian Doran
ae996e8847 feat(standalone/setup): dedicated back button 2026-03-25 20:07:37 +02:00
Elian Doran
06cb568fbd feat(standalone/setup): improve creating new document screen 2026-03-25 19:54:55 +02:00
Elian Doran
39a1aa360d feat(standalone/setup): pass information regarding demo 2026-03-25 19:42:42 +02:00
Elian Doran
51ed4dece2 feat(standalone/setup): page to select whether to import demo or not 2026-03-25 19:37:27 +02:00
Elian Doran
1620b0be62 chore(core): fix type issue with async import 2026-03-25 18:57:52 +02:00
Elian Doran
4c7c8a19c5 fix(standalone/setup): progress bar jumps back to zero before finishing sync 2026-03-25 18:51:19 +02:00
Elian Doran
93f825e970 chore(standalone): reduce verbosity of request errors 2026-03-25 18:49:01 +02:00
Elian Doran
310035be1b feat(standalone/setup): dedicated handling for wrong password 2026-03-25 18:48:38 +02:00
Elian Doran
4ec90e5575 feat(standalone/setup): dismissable error 2026-03-25 18:38:44 +02:00
Elian Doran
5ba5aee160 feat(standalone/setup): improve display of sync error 2026-03-25 18:25:47 +02:00
Elian Doran
aecca66972 style(standalone/setup): fix some spacing issues 2026-03-25 18:18:37 +02:00
Elian Doran
a872664789 feat(standalone/setup): use normal form groups for sync settings 2026-03-25 18:09:21 +02:00
Elian Doran
7b639f2718 refactor(standalone/setup): component for pages 2026-03-25 10:23:04 +02:00
Elian Doran
7dcc1496ec feat(standalone/setup): disable "Connect a desktop app" in standalone 2026-03-25 10:07:41 +02:00
Elian Doran
0dc7d71d1b style(standalone/setup): full-width footer 2026-03-25 10:00:47 +02:00
Elian Doran
dd67710b12 feat(standalone/setup): improve layout & design of sync in progress 2026-03-24 19:36:40 +02:00
Elian Doran
6d376731e3 fix(server): unable to do first setup 2026-03-24 19:13:49 +02:00
Elian Doran
5157fd9ecd feat(standalone/setup): decode slashes in error message 2026-03-24 18:42:53 +02:00
Elian Doran
4226827b5d chore(standalone/setup): improve error reporting 2026-03-24 18:38:06 +02:00
Elian Doran
cb3b362bad feat(standalone/setup): report errors in initial sync request 2026-03-24 18:24:29 +02:00
Elian Doran
4dcb08745b fix(standalone/setup): clicking on advanced options submits form 2026-03-24 16:49:23 +02:00
Elian Doran
28c57813db chore(standalone/setup): make fields required 2026-03-24 16:47:03 +02:00
Elian Doran
49868362cd chore(standalone/setup): add back proxy setting for server sync 2026-03-24 16:41:20 +02:00
Elian Doran
c2b965c24b fix(standalone/setup): lost connection to websocket 2026-03-24 16:31:44 +02:00
Elian Doran
6c3e16db20 chore(standalone/setup): basic spinner for desktop sync 2026-03-24 16:27:58 +02:00
Elian Doran
b880d81104 refactor(core): deduplicate some bootstrap items 2026-03-24 16:27:31 +02:00
Elian Doran
ef8db52ebe refactor(core): use different mechanism for shared bootstrap items 2026-03-24 16:24:59 +02:00
Elian Doran
185a88e655 fix(desktop): not starting due to lack of core initialization 2026-03-24 14:18:46 +02:00
Elian Doran
3eef1a1c59 chore(standalone/setup): improve layout of title 2026-03-24 13:55:12 +02:00
Elian Doran
78451b9721 feat(standalone/setup): add steps for desktop syncing 2026-03-24 13:25:04 +02:00
Elian Doran
26973681ec chore(standalone/setup): clarify syncing 2026-03-24 13:14:24 +02:00
Elian Doran
f48b67f872 feat(standalone/setup): add a sync illustration 2026-03-24 12:56:40 +02:00
Elian Doran
8d5ccb5ba8 chore(standalone/setup): add a nice background 2026-03-24 12:40:53 +02:00
Elian Doran
619751a8aa chore(standalone/setup): create empty page for sync from desktop 2026-03-24 12:32:33 +02:00
Elian Doran
be9c55acae feat(standalone/setup): add transition between pages 2026-03-24 12:17:55 +02:00
Elian Doran
ffd37755a3 chore(standalone/setup): fix typo in translation 2026-03-24 12:12:52 +02:00
Elian Doran
9991b8f1e2 feat(standalone/setup): intermediate screen for creating new document 2026-03-24 12:05:37 +02:00
Elian Doran
13eb8152e0 feat(standalone/setup): add syncing steps 2026-03-24 11:57:58 +02:00
Elian Doran
7bf6db7817 feat(standalone/setup): add a progress bar for sync status 2026-03-24 11:33:15 +02:00
Elian Doran
a1eb79fcb0 feat(standalone/setup): increase option creation speed 2026-03-24 10:30:45 +02:00
Elian Doran
3f5cdc533e feat(standalone/setup): sync from server without refresh 2026-03-24 10:18:23 +02:00
Elian Doran
697ea995cb fix(server): not detecting DB init state properly 2026-03-24 10:10:28 +02:00
Elian Doran
a2002b8e9c fix(server): not starting due to schema loading 2026-03-24 09:32:29 +02:00
Elian Doran
c1d8637fec chore(standalone/setup): bring back spinner 2026-03-24 09:09:21 +02:00
Elian Doran
b6ea29ffc9 chore(standalone/setup): basic sync page 2026-03-24 09:06:25 +02:00
Elian Doran
6aa0c573fb chore(standalone/setup): improve alignment of home screen 2026-03-24 08:59:08 +02:00
Elian Doran
fcc575c508 feat(standalone/setup): reload after creating new document 2026-03-23 23:05:57 +02:00
Elian Doran
62d6ce08a0 fix(standalone): database initialization slow 2026-03-23 21:35:26 +02:00
Elian Doran
b50127b0d3 fix(client): froca initialization incorrect due to DB init check 2026-03-23 21:29:38 +02:00
Elian Doran
669a58cc0e fix(standalone): database not initialized after first setup 2026-03-23 21:08:56 +02:00
Elian Doran
bf4b5dad5a feat(standalone/setup): set up new document 2026-03-23 21:06:30 +02:00
Elian Doran
39972a9bd7 feat(standalone/setup): basic server sync form 2026-03-23 20:27:44 +02:00
Elian Doran
44f519c1d6 feat(standalone/setup): basic footer 2026-03-23 20:21:47 +02:00
Elian Doran
dd6c5bbf12 chore(standalone/setup): more concise descriptions 2026-03-23 20:10:56 +02:00
Elian Doran
20d4db2608 style(standalone/setup): add a shadow 2026-03-23 20:07:32 +02:00
Elian Doran
3151e86665 feat(standalone/setup): add icons 2026-03-23 20:02:20 +02:00
Elian Doran
96a0d483f5 feat(standalone/setup): add hover effect 2026-03-23 19:50:53 +02:00
Elian Doran
3faefdbc85 feat(standalone/setup): basic styling of cards 2026-03-23 19:47:44 +02:00
Elian Doran
12347d5c4a chore(standalone/setup): basic layout 2026-03-23 19:30:00 +02:00
Elian Doran
4dbaadf9cc chore(standalone/setup): replace properly for hot reload 2026-03-23 19:26:26 +02:00
Elian Doran
2a1c165a54 fix(standalone/setup): translations not initializing due to missing asset path 2026-03-23 19:25:01 +02:00
Elian Doran
939f931809 chore(standalone/setup): setup translation partially 2026-03-23 19:20:30 +02:00
Elian Doran
4fd09bf1f8 chore(standalone/setup): prevent error in froca due to not initialized DB 2026-03-23 19:20:24 +02:00
Elian Doran
3231db3c3f fix(standalone/setup): server API missing when DB not initialized 2026-03-23 19:19:56 +02:00
Elian Doran
c07ea1bfa7 feat(standalone/setup): dedicated setup page with React 2026-03-23 18:59:56 +02:00
Elian Doran
79db638bf4 chore(standalone): get bootstrap to report not initialized state 2026-03-23 18:54:44 +02:00
Elian Doran
794dab2894 chore(standalone): port most of sql_init 2026-03-23 18:49:06 +02:00
Elian Doran
97b303aea6 chore(standalone): remove default seed 2026-03-23 18:34:16 +02:00
Elian Doran
a259b65085 feat(core): port image route 2026-03-23 17:11:09 +02:00
Elian Doran
5ea014cc37 fix(standalone): component ID not preserved in WS 2026-03-23 16:47:28 +02:00
Elian Doran
3210dbb6d8 feat(core): integrate similar_notes route 2026-03-23 16:29:59 +02:00
Elian Doran
64cbb2c7d2 Revert "chore(client): bypass autocomplete count for now"
This reverts commit b19bf62d7e.
2026-03-23 16:20:44 +02:00
Elian Doran
3b35dc50c5 feat(core): integrate autocomplete route 2026-03-23 16:20:18 +02:00
Elian Doran
a768d2f7a7 chore(core): relative imports broken by base path 2026-03-23 16:17:41 +02:00
Elian Doran
156ac3be6d Feature/standalone ws (#9143) 2026-03-22 23:23:03 +02:00
Elian Doran
ccc0038d4e chore(server): fix type issue 2026-03-22 23:04:51 +02:00
Elian Doran
3684f4727c Feature/standalone search integration (#9139) 2026-03-22 22:51:57 +02:00
Elian Doran
efd294d53b fix(search): wrong escape of highlighted tokens 2026-03-22 21:52:14 +02:00
Elian Doran
f9eb4bf574 chore(core): address requested changes 2026-03-22 21:48:40 +02:00
Elian Doran
b49912bf71 fix(standalone): sync failing due to credentials 2026-03-22 21:23:05 +02:00
Elian Doran
f5f11de58e fix(standalone): sync crashing due to use of Buffer 2026-03-22 21:18:39 +02:00
Elian Doran
a8ea40b2e1 fix(standalone): missing hmac implementation 2026-03-22 21:00:15 +02:00
Elian Doran
308bab8a3c fix(server): CORS for syncing with standalone 2026-03-22 20:53:19 +02:00
Elian Doran
ef8c4cef8a fix(server): web socket initialization not working 2026-03-22 20:44:07 +02:00
Elian Doran
63198a03ab fix(server): imports after moving to core 2026-03-22 20:38:41 +02:00
Elian Doran
ed808abd22 fix(core): sync-related issues 2026-03-22 20:17:48 +02:00
Elian Doran
9fe23442f5 chore(core): integrate content_hash 2026-03-22 20:10:59 +02:00
Elian Doran
0e2e86e7d3 chore(core): integrate consistency_checks 2026-03-22 20:09:18 +02:00
Elian Doran
ea0e3fd248 chore(core): integrate sync service and route 2026-03-22 20:02:08 +02:00
Elian Doran
2ac85a1d1c chore(core): add provider for requests 2026-03-22 19:32:51 +02:00
Elian Doran
cb71dc4202 chore(standalone): wrap requests 2026-03-22 19:17:38 +02:00
Elian Doran
6637542e7c chore(git): ignore Claude local settings 2026-03-22 19:15:53 +02:00
Elian Doran
971ce09811 chore(standalone): remove superfluos log for requests 2026-03-22 19:14:39 +02:00
Elian Doran
04826074f4 fix(standalone): error in WS initialization 2026-03-22 19:13:40 +02:00
Elian Doran
bcd4baff3d feat(standalone): basic WS functionality 2026-03-22 19:11:08 +02:00
Elian Doran
3bcf7b22be chore(standalone): add workspace-level run script 2026-03-22 19:00:17 +02:00
Elian Doran
ee8c54bdd3 chore(core): integrate sync mutex 2026-03-22 19:00:04 +02:00
Elian Doran
1af8699fc0 chore(core): integrate CLS getAndClearEntityChangeIds 2026-03-22 18:56:22 +02:00
Elian Doran
5bc1fc71ef chore(standalone/wasm): different client-side subscriber 2026-03-22 16:12:27 +02:00
Elian Doran
0b5ce95093 fix(standalone): some sql queries not executing properly 2026-03-22 15:48:40 +02:00
Elian Doran
77971a10d1 feat(core): integrate special notes with route 2026-03-22 14:30:33 +02:00
Elian Doran
28a56ff7bf feat(core): integrate search with route 2026-03-22 14:03:48 +02:00
Elian Doran
d7d28bcf58 chore(standalone): align version with the rest 2026-03-22 13:37:52 +02:00
Elian Doran
682e1549f8 fix(standalone): failing due to type error 2026-03-22 13:03:54 +02:00
Elian Doran
d7d2b21935 feat(standalone): improve error handling on initialization 2026-03-22 13:02:50 +02:00
Elian Doran
1b7d2da6cb Merge remote-tracking branch 'origin/main' into standalone
; Conflicts:
;	apps/client/src/layouts/mobile_layout.tsx
;	apps/client/src/services/promoted_attribute_definition_parser.ts
;	apps/server/package.json
;	apps/server/src/becca/entities/bnote.ts
;	apps/server/src/etapi/etapi_utils.ts
;	apps/server/src/etapi/notes.ts
;	apps/server/src/routes/api/clipper.ts
;	apps/server/src/routes/api/export.ts
;	apps/server/src/routes/api/files.ts
;	apps/server/src/routes/api/image.ts
;	apps/server/src/routes/api/import.ts
;	apps/server/src/routes/api/note_map.ts
;	apps/server/src/routes/api/search.ts
;	apps/server/src/routes/api/similar_notes.ts
;	apps/server/src/routes/api/sync.ts
;	apps/server/src/routes/error_handlers.ts
;	apps/server/src/routes/index.ts
;	apps/server/src/routes/route_api.ts
;	apps/server/src/routes/routes.ts
;	apps/server/src/services/anonymization.ts
;	apps/server/src/services/app_info.ts
;	apps/server/src/services/builtin_attributes.ts
;	apps/server/src/services/export/zip.ts
;	apps/server/src/services/hidden_subtree.ts
;	apps/server/src/services/llm/ai_service_manager.ts
;	apps/server/src/services/llm/context/modules/context_formatter.ts
;	apps/server/src/services/llm/context/note_content.ts
;	apps/server/src/services/llm/formatters/base_formatter.ts
;	apps/server/src/services/llm/formatters/ollama_formatter.ts
;	apps/server/src/services/llm/formatters/openai_formatter.ts
;	apps/server/src/services/llm/tools/read_note_tool.ts
;	apps/server/src/services/note_types.ts
;	apps/server/src/services/notes.ts
;	apps/server/src/services/options.ts
;	apps/server/src/services/options_init.ts
;	apps/server/src/services/search/expressions/note_content_fulltext.ts
;	apps/server/src/services/utils.ts
;	apps/server/src/services/ws.ts
;	apps/server/src/share/content_renderer.ts
;	packages/commons/src/lib/builtin_attributes.ts
;	packages/commons/src/lib/rows.ts
;	packages/trilium-core/src/routes/api/attachments.ts
;	packages/trilium-core/src/routes/api/attributes.ts
;	packages/trilium-core/src/routes/api/branches.ts
;	packages/trilium-core/src/routes/api/notes.ts
;	packages/trilium-core/src/routes/api/recent_changes.ts
;	packages/trilium-core/src/routes/api/revisions.ts
;	packages/trilium-core/src/routes/api/sql.ts
;	packages/trilium-core/src/routes/api/stats.ts
;	packages/trilium-core/src/services/attributes.ts
;	packages/trilium-core/src/services/builtin_attributes.ts
;	packages/trilium-core/src/services/promoted_attribute_definition_parser.ts
;	pnpm-lock.yaml
2026-03-22 12:56:14 +02:00
Elian Doran
9350c43e5b chore(core): port bulk actions route 2026-02-09 19:49:07 +02:00
Elian Doran
0fae11d54c chore(core): port bulk actions service 2026-02-09 19:46:34 +02:00
Elian Doran
1ed3999639 chore(core): port recent changes route 2026-02-09 19:43:53 +02:00
Elian Doran
7d30771f05 chore(core): port relation map route 2026-02-09 19:41:31 +02:00
Elian Doran
08f1d44d90 chore(core): port revisions route 2026-02-09 19:38:24 +02:00
Elian Doran
969860c344 chore(core): port attribute route 2026-02-09 19:32:46 +02:00
Elian Doran
ed905c9d64 chore(core): integrate builtin_attributes 2026-02-09 19:29:59 +02:00
Elian Doran
c5518b64b7 chore(core): integrate attribute_formatter 2026-02-09 19:24:06 +02:00
Elian Doran
a7b2b631c5 feat(standalone): add warning about stability 2026-02-09 18:59:44 +02:00
Elian Doran
dcfc1119eb chore(core): port sql route 2026-02-09 18:38:51 +02:00
Elian Doran
88add55ebc chore(standalone): wrap routes in a transaction 2026-02-09 18:35:29 +02:00
Elian Doran
ad41a58904 chore(standalone): use CLS with per-request context isolation 2026-02-09 18:20:14 +02:00
Elian Doran
49ce312ab2 chore(standalone): use a simpler CLS mechanism considering lack of multi-threading 2026-02-09 18:16:15 +02:00
Elian Doran
223d69206c fix(standalone): missing context menu cover 2026-02-09 18:00:11 +02:00
Elian Doran
d68ada1026 fix(standalone): translations not working in prod 2026-02-08 22:38:28 +02:00
Elian Doran
e0a23f6b63 fix(bootstrap): background effects are enabled 2026-02-08 21:30:19 +02:00
Elian Doran
bd147ea72e Merge remote-tracking branch 'origin/main' into standalone 2026-02-08 21:14:12 +02:00
Elian Doran
4494aed1cf chore(standalone): use async for init 2026-01-30 15:55:20 +02:00
Elian Doran
788eaad61c fix(standalone): wrong server translation path in production 2026-01-30 15:49:32 +02:00
Elian Doran
0cfd6bae0e refactor(standalone): use different mechanism for importing local server worker 2026-01-30 15:24:53 +02:00
Elian Doran
82c435b916 chore(ci): deploy app on workflow change 2026-01-30 07:55:21 +02:00
Elian Doran
bc5b9708c7 Merge remote-tracking branch 'origin/main' into standalone 2026-01-30 07:51:36 +02:00
Elian Doran
7e87e6f832 chore(ci): deploy app on standalone branch 2026-01-30 07:48:11 +02:00
Elian Doran
e5a7a32439 chore(core): port cloning route 2026-01-29 22:20:54 +02:00
Elian Doran
e9214d84b7 chore(core): port stats route 2026-01-29 21:51:47 +02:00
Elian Doran
da7a61a8b6 Merge remote-tracking branch 'origin/main' into HEAD
; Conflicts:
;	apps/client/src/index.ts
;	apps/client/src/widgets/sql_table_schemas.tsx
;	apps/server/package.json
;	apps/server/src/app.ts
;	apps/server/src/becca/entities/bnote.ts
;	apps/server/src/services/import/single.ts
;	apps/server/src/services/import/zip.ts
;	apps/server/src/services/note-interface.ts
;	apps/server/src/services/notes.ts
;	apps/server/src/services/tree.ts
;	apps/server/src/services/utils.ts
;	apps/server/src/share/shaca/entities/snote.ts
;	pnpm-lock.yaml
;	scripts/update-nightly-version.ts
;	scripts/update-version.ts
2026-01-29 21:47:06 +02:00
Elian Doran
458e858b24 fix(standalone): error due to SQL returning bigint 2026-01-17 20:01:46 +02:00
Elian Doran
dd35a49752 chore(standalone-desktop): add title 2026-01-17 11:33:35 +02:00
Elian Doran
b06126baf3 chore(standalone-desktop): basic exit mechanism 2026-01-17 11:32:27 +02:00
Elian Doran
9d86b43909 feat(standalone-desktop): basic dev server 2026-01-17 11:23:44 +02:00
Elian Doran
79432c117c chore(standalone-desktop): trigger Neutralino build 2026-01-17 10:01:12 +02:00
Elian Doran
77371b6658 chore(standalone-desktop): prepare resources for dist 2026-01-17 09:31:21 +02:00
Elian Doran
6ca6b0a1e4 Merge remote-tracking branch 'origin/standalone' into feature/standalone_neutralino 2026-01-17 09:19:56 +02:00
Elian Doran
3e6678ad9e chore(standalone-desktop): init with Neutralino 2026-01-17 09:07:27 +02:00
Elian Doran
ec84e72b4c Lightweight/browser api (#8287) 2026-01-14 18:30:05 +02:00
Elian Doran
64a8c3b005 chore(client-standalone): address requested changes 2026-01-14 18:27:53 +02:00
Elian Doran
0b5cf2e6c8 Merge remote-tracking branch 'origin/standalone' into lightweight/browser_api 2026-01-14 18:04:54 +02:00
Elian Doran
7ed4e1c284 Lightweight/decouple server api (#8284) 2026-01-14 18:01:54 +02:00
Elian Doran
9dd7616f7d chore(client-standalone): address requested changes 2026-01-14 18:00:10 +02:00
Elian Doran
ab29caff7b fix(client-standalone): CK premium features not working 2026-01-14 17:48:29 +02:00
Elian Doran
7633e3d48e chore(client-standalone): address requested changes 2026-01-14 17:41:24 +02:00
Elian Doran
411fdf3114 chore(client-standalone): disable WS error notification 2026-01-14 17:33:57 +02:00
Elian Doran
5c52917459 fix(client-standalone): webmanifest icon path not correct 2026-01-14 17:31:06 +02:00
Elian Doran
51753ad82a chore(ci): run tests on standalone branch as well 2026-01-12 21:51:26 +02:00
Elian Doran
7e00634f3d chore(deps): align package lock 2026-01-12 21:44:25 +02:00
Elian Doran
daf41804d4 chore(core): address requested changes 2026-01-12 21:43:57 +02:00
Elian Doran
43d087f886 chore(deps): update lock file 2026-01-12 21:32:06 +02:00
Elian Doran
503a6e520d Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-12 21:31:32 +02:00
Elian Doran
52610a7410 fix(client-standalone): missing manifest 2026-01-12 21:06:00 +02:00
Elian Doran
c7edb71fed fix(client-standalone): missing favicon 2026-01-12 21:05:21 +02:00
Elian Doran
83db37ed31 fix(server): app-info not showing data dir 2026-01-12 21:03:55 +02:00
Elian Doran
0d1c8ae01e fix(server): login not working due to bad import to i18n 2026-01-12 20:55:32 +02:00
Elian Doran
92f71e100f chore(core): integrate app_info route 2026-01-12 20:54:18 +02:00
Elian Doran
659573b864 fix(client-standalone): update version to match 2026-01-12 20:50:12 +02:00
Elian Doran
e1c798561b fix(client-standalone): user guide not working 2026-01-12 20:46:08 +02:00
Elian Doran
0c52b56e02 chore(core): integrate branches service and route 2026-01-12 19:25:45 +02:00
Elian Doran
f9731d9cfc chore(text): re-enable emojis 2026-01-12 19:00:35 +02:00
Elian Doran
7547371ba0 feat(client-standalone): proper integration of server-side locale 2026-01-12 18:44:48 +02:00
Elian Doran
84e1d45d2a fix(client-standalone): print not working 2026-01-11 23:05:27 +02:00
Elian Doran
364c9cda27 chore(client-standalone): reduce verbosity in logs for requests 2026-01-11 23:05:26 +02:00
Elian Doran
af944c29a8 feat(client-standalone): support more globals 2026-01-11 23:04:53 +02:00
Elian Doran
45577f1585 feat(client-standalone): better device detection 2026-01-11 23:04:53 +02:00
Elian Doran
1648c67467 feat(client-standalone): initialize server-side translations 2026-01-11 22:46:47 +02:00
Elian Doran
882793e794 chore(client-standalone): basic support for mobile 2026-01-11 18:29:47 +02:00
Elian Doran
4a4a7d79c2 chore(client-standalone): integrate faster preact 2026-01-11 17:52:56 +02:00
Elian Doran
a955eb80da chore(client-standalone): integrate main client script 2026-01-11 17:34:25 +02:00
Elian Doran
cd64a1ee18 chore(client-standalone): fix noscript 2026-01-11 17:31:15 +02:00
Elian Doran
9894d4256c chore(deps): update lock 2026-01-11 17:31:07 +02:00
Elian Doran
3b5f1dabd6 Merge remote-tracking branch 'origin/lightweight/decouple_server_api' into lightweight/browser_api 2026-01-11 17:21:37 +02:00
Elian Doran
750fa2e647 Merge remote-tracking branch 'origin/main' into lightweight/decouple_server_api 2026-01-11 17:15:35 +02:00
Elian Doran
4f0021e44e Merge remote-tracking branch 'origin/main' into lightweight/browser_api
; Conflicts:
;	apps/client/src/widgets/layout/StatusBar.tsx
2026-01-07 19:41:51 +02:00
Elian Doran
2546e4c0dc fix(client): server worker in client 2026-01-07 18:29:00 +02:00
Elian Doran
eac5dbb210 chore(client-standalone): async-proxy missing in prod 2026-01-07 17:58:12 +02:00
Elian Doran
8b6da981f7 chore(client-standalone): try to use plain header file 2026-01-07 17:49:25 +02:00
Elian Doran
7433ca069f chore(client-standalone): wrong file name to CORS 2026-01-07 17:35:37 +02:00
Elian Doran
128049b672 chore(core): integrate icon usage API 2026-01-07 17:33:44 +02:00
Elian Doran
0eb3cb1118 feat(client-standalone): proper startup without requiring refresh 2026-01-07 17:19:52 +02:00
Elian Doran
8fc28716a7 feat(client-standalone): set up CORS for Cloudflare Pages 2026-01-07 17:14:31 +02:00
Elian Doran
af346f455a fix(client-standalone): version check was broken 2026-01-07 16:53:37 +02:00
Elian Doran
3e5a6c1e51 chore(client-standalone): fake two more routes 2026-01-07 16:43:17 +02:00
Elian Doran
9e3b4435cd fix(client): request to recent changes for undefined note 2026-01-07 16:43:11 +02:00
Elian Doran
3a793a3549 chore(client-standalone): fake two more routes 2026-01-07 16:41:19 +02:00
Elian Doran
4f139552f4 chore(core): integrate recent-notes 2026-01-07 16:41:08 +02:00
Elian Doran
13f25e9fed chore(client-standalone): integrate note map backlink count 2026-01-07 16:36:49 +02:00
Elian Doran
91db73703b chore(client-standalone): add two dummy routes 2026-01-07 16:32:29 +02:00
Elian Doran
d690985b58 fix(client): SQL schemas loaded even when not needed 2026-01-07 16:27:48 +02:00
Elian Doran
b5bcf73531 chore(client-standalone): bring back window.global 2026-01-07 16:21:35 +02:00
Elian Doran
2e905c8292 fix(deps): lock file out of sync 2026-01-07 16:06:15 +02:00
Elian Doran
4374c92032 feat(ci): add deployment script for standalone client 2026-01-07 16:04:04 +02:00
Elian Doran
edde0d0f90 fix(client-standalone): get it to start in prod 2026-01-07 15:50:34 +02:00
Elian Doran
32c39384ff fix(client-standalone): missing entry point for sw, local-bridge, local-server-worker 2026-01-07 15:20:59 +02:00
Elian Doran
807ab4be8c fix(client-standalone): build missing .wasm 2026-01-07 15:16:38 +02:00
Elian Doran
4da20f4829 fix(client-standalone): some assets could not be loaded 2026-01-07 15:11:01 +02:00
Elian Doran
cb5b491633 fix(client-standalone): get client scripts to run 2026-01-07 14:42:02 +02:00
Elian Doran
e76c33c37a chore(client-standalone): relocate index file to root 2026-01-07 14:34:41 +02:00
Elian Doran
89fc89603e chore(client-standalone): set up live reload for assets 2026-01-07 14:30:29 +02:00
Elian Doran
c0bf294457 chore(client-standalone): basic integration for assets 2026-01-07 14:29:23 +02:00
Elian Doran
24e076cacf chore(client-standalone): integrate new files from client 2026-01-07 14:22:04 +02:00
Elian Doran
1e381b13ca chore(client-standalone): create empty project 2026-01-07 14:14:52 +02:00
Elian Doran
f83121ce1d chore(core): integrate attachments route 2026-01-07 13:48:59 +02:00
Elian Doran
b32480f1d3 feat(client/lightweight): basic WS support 2026-01-07 13:42:42 +02:00
Elian Doran
d4468bd97b feat(client/lightweight): basic OPFS support for persistence 2026-01-07 13:27:17 +02:00
Elian Doran
e8711d7cd5 fix(client/lightweight): not handling returning backend entities 2026-01-07 13:04:24 +02:00
Elian Doran
35f4d2aaad chore(client/lightweight): improve route error handling 2026-01-07 12:55:58 +02:00
Elian Doran
b1f3fe5345 fix(client/lightweight): saving not working 2026-01-07 12:53:07 +02:00
Elian Doran
9f1b0ac449 fix(client/lightweight): saved statements causing issues 2026-01-07 12:41:08 +02:00
Elian Doran
a84e804fc3 fix(client/lightweight): CLS not available in routes 2026-01-07 12:37:29 +02:00
Elian Doran
3371a31c70 fix(client/lightweight): crypto hash not working 2026-01-07 12:32:45 +02:00
Elian Doran
724af8e103 fix(client/lightweight): statements with parameters not working 2026-01-07 12:21:27 +02:00
Elian Doran
c5803a2650 fix(client/lightweight): missing pluck implementation 2026-01-07 12:16:09 +02:00
Elian Doran
baf18835be fix(client/lightweight): SQL nested transactions not supported 2026-01-07 12:14:30 +02:00
Elian Doran
3d1c93e58c fix(client/lightweight): note content not rendering 2026-01-07 12:07:49 +02:00
Elian Doran
ab0800a9f3 chore(core): integrate notes route 2026-01-07 12:00:38 +02:00
Elian Doran
dd58eac4b0 fix(client/lightweight): boxicons not loading 2026-01-07 11:50:25 +02:00
Elian Doran
c6d1457ad7 refactor(client/lightweight): bootstrap route as part of the new router 2026-01-07 11:48:22 +02:00
Elian Doran
f05fda871c chore(core): integrate icon_packs service 2026-01-07 11:45:40 +02:00
Elian Doran
22590596da feat(core): shared router between lightweight and server 2026-01-07 11:37:50 +02:00
Elian Doran
8274f9a220 feat(client): lightweight router implementation 2026-01-07 11:30:52 +02:00
Elian Doran
b19bf62d7e chore(client): bypass autocomplete count for now 2026-01-07 11:26:31 +02:00
Elian Doran
7b436bdf70 chore(client): vite not reloading core module 2026-01-07 11:24:04 +02:00
Elian Doran
a1c4a17d64 chore(core): integrate keyboard actions route 2026-01-07 11:23:46 +02:00
Elian Doran
7966cfd09c chore(client/lightweight): wait for becca to load before processing requests 2026-01-07 11:13:25 +02:00
Elian Doran
0fe299250e chore(client/lightweight): tree route import not seen 2026-01-07 11:07:23 +02:00
Elian Doran
adfe490480 chore(core): fix import 2026-01-07 10:43:02 +02:00
Elian Doran
872ab0864b chore(client/lightweight): handle routes properly 2026-01-06 23:19:41 +02:00
Elian Doran
6633b4233d chore(client/lightweight): initialize database earlier 2026-01-06 23:05:40 +02:00
Elian Doran
a2d873d16f chore(client/lightweight): port tree integration 2026-01-06 22:59:18 +02:00
Elian Doran
a6f52fff3e fix(client/lightweight): raw SQL queries not working 2026-01-06 22:20:53 +02:00
Elian Doran
7832f20c89 feat(client/lightweight): import demo database 2026-01-06 21:45:02 +02:00
Elian Doran
405db7cedb chore(client/lightweight): fix errors in SQL provider & implement crypto provider 2026-01-06 21:05:53 +02:00
Elian Doran
ccf4df8e86 chore(client/lightweight): basic SQL implementation 2026-01-06 20:59:52 +02:00
Elian Doran
1beda05e6c chore(client/lightweight): basic CLS implementation 2026-01-06 20:59:49 +02:00
Elian Doran
18a3d9d71a fix(client/lightweight): TypeScript not processed 2026-01-06 20:39:11 +02:00
Elian Doran
25dc9201bf feat(client/lightweight): improve error handling 2026-01-06 20:27:35 +02:00
Elian Doran
b60501dd3f chore(core) integrate options route 2026-01-06 20:12:03 +02:00
Elian Doran
cbd2fc3966 chore(client/lightweight): fix asset and API base path 2026-01-06 19:41:31 +02:00
Elian Doran
9bce12a85b Merge remote-tracking branch 'origin/lightweight/decouple_server_api' into lightweight/browser_api 2026-01-06 19:33:35 +02:00
Elian Doran
8523c369e1 fix(server): imports preventing start-up 2026-01-06 19:15:53 +02:00
Elian Doran
7c16aeca4a chore(core): crash due to dbReady before CLS init 2026-01-06 16:30:45 +02:00
Elian Doran
8399600e79 chore(core): address some missing methods in utils 2026-01-06 16:29:30 +02:00
Elian Doran
edac58f3fa chore(core): integrate revisions 2026-01-06 16:24:14 +02:00
Elian Doran
51d0d848c5 chore(core): no-op request service 2026-01-06 16:22:47 +02:00
Elian Doran
1edab8e8da chore(core): no-op image service 2026-01-06 16:21:42 +02:00
Elian Doran
e1e294914a chore(core): no-op search 2026-01-06 16:20:10 +02:00
Elian Doran
4668fdc15c chore(core): no-op sqlInit 2026-01-06 16:18:06 +02:00
Elian Doran
f1e0d5558c chore(core): integrate erase 2026-01-06 16:16:54 +02:00
Elian Doran
c94c54c641 chore(core): integrate task_context with ws no-op 2026-01-06 16:09:21 +02:00
Elian Doran
18416eb89a chore(core): no op script 2026-01-06 16:07:30 +02:00
Elian Doran
263c9028e2 chore(core): integrate hidden_subtree 2026-01-06 16:05:16 +02:00
Elian Doran
0b528e9937 chore(core): integrate handlers 2026-01-06 15:57:36 +02:00
Elian Doran
e905c1ec11 chore(core): integrate cloning service 2026-01-06 15:52:37 +02:00
Elian Doran
ecb27fe9f7 chore(core): integrate tree service 2026-01-06 15:48:48 +02:00
Elian Doran
a8f6db4b20 chore(core): fix some imports 2026-01-06 15:45:07 +02:00
Elian Doran
78262e55ec chore(core): integrate escape/unescape & toMap 2026-01-06 15:43:36 +02:00
Elian Doran
c6197e520d chore(core): integrate some more utils 2026-01-06 15:41:34 +02:00
Elian Doran
299c06c1a6 chore(core): fix inaccessible NoteParams 2026-01-06 15:33:48 +02:00
Elian Doran
674593b38c chore(core): integrate html_sanitizer 2026-01-06 15:31:22 +02:00
Elian Doran
f5535657ad chore(core): port note_types 2026-01-06 15:17:10 +02:00
Elian Doran
de4d07e904 chore(core): get rid of note_interface 2026-01-06 15:15:12 +02:00
Elian Doran
5508b505c8 chore(core): port notes service partially 2026-01-06 15:14:08 +02:00
Elian Doran
8cdfc108ba fix(core): wrong imports to src 2026-01-06 13:52:57 +02:00
Elian Doran
6a0f6fab83 fix(core): server not starting due to crypto not initialized 2026-01-06 13:50:22 +02:00
Elian Doran
ad3be73e1b chore(core): integrate note_set 2026-01-06 13:45:53 +02:00
Elian Doran
64b212b93e chore(core): integrate entity_changes 2026-01-06 13:42:29 +02:00
Elian Doran
60cb8d950e chore(core): integrate promoted_attribute_definition_parser 2026-01-06 13:30:21 +02:00
Elian Doran
61f6f94295 chore(core): integrate sanitize_attribute_name 2026-01-06 13:26:19 +02:00
Elian Doran
ebe7276f40 chore(core): fix some use of logs 2026-01-06 13:21:39 +02:00
Elian Doran
26d299aa44 chore(core): integrate options, options_init & keyboard_actions 2026-01-06 13:20:42 +02:00
Elian Doran
bd45c32251 chore(core): integrate utils partially 2026-01-06 13:06:14 +02:00
Elian Doran
321558a01f chore(core): fix minor type issue 2026-01-06 12:43:45 +02:00
Elian Doran
f5a77477aa chore(core): fix missing CLS method 2026-01-06 12:42:35 +02:00
Elian Doran
20c90d1296 chore(server): fix incompatibility with Uint8Array 2026-01-06 12:40:43 +02:00
Elian Doran
bbfef0315f chore(core): fix incompatibility with Uint8Array 2026-01-06 12:34:16 +02:00
Elian Doran
321fcf34f2 chore(core): fix references to getHoistedNoteId 2026-01-06 12:29:13 +02:00
Elian Doran
b9a59fe0c4 chore(core): fixs some imports to protected_session 2026-01-06 12:27:00 +02:00
Elian Doran
01f3c32d92 refactor(server): remove Blob interface in favor of BlobRow 2026-01-06 12:24:09 +02:00
Elian Doran
05b9e2ec2a chore(core): fix references to core 2026-01-06 12:20:01 +02:00
Elian Doran
c8d3b091fd chore(commons): fix Node reference 2026-01-06 12:19:42 +02:00
Elian Doran
d717a89163 chore(core): fix references to Buffer 2026-01-06 12:16:38 +02:00
Elian Doran
8149460547 chore(commons): fix issues with Buffer 2026-01-06 12:07:16 +02:00
Elian Doran
b7ad76827a chore(server): various references to core 2026-01-06 12:05:52 +02:00
Elian Doran
e19e9b3830 chore(core): fix references to blob-service 2026-01-06 12:01:18 +02:00
Elian Doran
40b07c3e8a chore(core): fix references to becca-interface 2026-01-06 11:59:45 +02:00
Elian Doran
a15b84b4e5 chore(core): fix type error in getFlatText 2026-01-06 11:56:52 +02:00
Elian Doran
544c52931c chore(server): fix references to abstract becca entity 2026-01-06 11:55:10 +02:00
Elian Doran
9391159413 chore(server): fix references to becca service 2026-01-06 11:52:25 +02:00
Elian Doran
f9e22a9ba9 chore(server): fix references to becca loader 2026-01-06 11:49:22 +02:00
Elian Doran
f88ac5dfae chore(server): fix imports to becca entities 2026-01-06 11:46:15 +02:00
Elian Doran
3459d2906e chore(server): fix imports to validation error 2026-01-06 11:41:06 +02:00
Elian Doran
4506b717d5 chore(server): fix imports to becca 2026-01-06 11:38:25 +02:00
Elian Doran
af8744ef2a chore(core): integrate errors 2026-01-06 11:31:13 +02:00
Elian Doran
320d8e3b45 chore(core): partially integrate becca 2026-01-06 11:23:52 +02:00
Elian Doran
c20da77f83 chore(core): integrate events service 2026-01-06 11:10:09 +02:00
Elian Doran
14e2e85da7 chore(core): integrate date_utils 2026-01-06 11:03:33 +02:00
Elian Doran
c7f0d541c2 fix(server): blob errors out 2026-01-06 10:41:50 +02:00
Elian Doran
5d474150da feat(client/lightweight): integrate SQLite 2026-01-05 20:00:00 +02:00
Elian Doran
d3941752f1 chore(client/lightweight): disable caching for now 2026-01-05 19:10:25 +02:00
Elian Doran
56b305b1de fix(client/lightweight): html aggressively cached 2026-01-05 18:50:09 +02:00
Elian Doran
bde472d649 feat(client/standalone): basic service worker attempt 2026-01-05 18:35:14 +02:00
Elian Doran
c1548b0f54 chore(server): integrate data_encryption, and protected_session 2026-01-05 17:47:25 +02:00
Elian Doran
6f04738629 chore(core): add documentation for SQL 2026-01-05 16:07:17 +02:00
Elian Doran
f79af7b045 fix(server): request content empty due to CLS 2026-01-05 16:01:27 +02:00
Elian Doran
527f502083 fix(server): requests failing due to cls namespacing issue 2026-01-05 15:58:24 +02:00
Elian Doran
d61e2c6f2c chore(server): get DB to be loaded 2026-01-05 15:52:31 +02:00
Elian Doran
ea31d2f446 chore(core): basic integration of SQL + CLS + log 2026-01-05 15:45:45 +02:00
Elian Doran
62803a1817 chore(server): set up dependency to trilium-core 2026-01-05 14:42:32 +02:00
Elian Doran
a67464b4a0 refactor(server): decouple bettersqlite3 from sql service 2026-01-05 14:03:03 +02:00
Elian Doran
00e7482968 chore(core): create empty package 2026-01-05 12:26:13 +02:00
830 changed files with 33844 additions and 45257 deletions

View File

@@ -186,14 +186,6 @@ When adding query parameters to ETAPI endpoints (`apps/server/src/etapi/`), main
**Auth note**: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
### 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.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations or `needsContext: true` for tools that need the current note context
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
### Database Migrations
- Add scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -221,12 +213,6 @@ Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/`
10. **Attribute inheritance can be complex** - When checking for labels/relations, use `note.getOwnedAttribute()` for direct attributes or `note.getAttribute()` for inherited ones. Don't assume attributes are directly on the note.
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## TypeScript Configuration
- **Project references**: Monorepo uses TypeScript project references (`tsconfig.json`)
@@ -289,12 +275,6 @@ View types are configured via `#viewType` label (e.g., `#viewType=table`). Each
- Register in `packages/ckeditor5/src/plugins.ts`
- See `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid` for examples
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
- Update schema in `apps/server/src/assets/db/schema.sql`
@@ -319,8 +299,6 @@ Trilium provides powerful user scripting capabilities:
- Translation files in `apps/client/src/translations/`
- 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

67
.github/workflows/deploy-app.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Deploy Standalone App
on:
# Trigger on push to main branch
push:
branches:
- standalone
# Only run when app files change
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
# Allow manual triggering from Actions tab
workflow_dispatch:
# Run on pull requests for preview deployments
pull_request:
paths:
- 'apps/client/**'
- 'apps/client-standalone/**'
- 'packages/trilium-core/**'
- '.github/workflows/deploy-app.yml'
jobs:
build-and-deploy:
name: Build and Deploy App
runs-on: ubuntu-latest
timeout-minutes: 10
# Required permissions for deployment
permissions:
contents: read
deployments: write
pull-requests: write # For PR preview comments
id-token: write # For OIDC authentication (if needed)
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Trigger build of app
run: pnpm --filter=client-standalone build
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
if: github.repository == vars.REPO_MAIN
with:
project_name: "trilium-app"
comment_body: "🖥️ App preview is ready"
production_url: "https://app.triliumnotes.org"
deploy_dir: "apps/client-standalone/dist"
cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
cloudflare_account_id: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,13 +1,9 @@
name: Dev
on:
push:
branches:
- main
- "release/*"
branches: [ main, standalone ]
pull_request:
branches:
- main
- "release/*"
branches: [ main, standalone ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -69,8 +69,6 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
npm_config_package_import_method: copy
- name: Update nightly version
run: pnpm run chore:ci-update-nightly-version
- name: Run the build

3
.gitignore vendored
View File

@@ -51,3 +51,6 @@ upload
site/
apps/*/coverage
scripts/translation/.language*.json
# AI
.claude/settings.local.json

View File

@@ -1,8 +0,0 @@
{
"mcpServers": {
"trilium": {
"type": "http",
"url": "http://localhost:8080/mcp"
}
}
}

324
CLAUDE.md
View File

@@ -4,209 +4,197 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages.
## Development Commands
### Setup
- `pnpm install` - Install all dependencies
- `corepack enable` - Enable pnpm if not available
```bash
# Setup
corepack enable && pnpm install
### Running Applications
- `pnpm run server:start` - Start development server (http://localhost:8080)
- `pnpm run server:start-prod` - Run server in production mode
# Run
pnpm server:start # Dev server at http://localhost:8080
pnpm desktop:start # Electron dev app
pnpm standalone:start # Standalone client dev
### Building
- `pnpm run client:build` - Build client application
- `pnpm run server:build` - Build server application
- `pnpm run electron:build` - Build desktop application
# Build
pnpm client:build # Frontend
pnpm server:build # Backend
pnpm desktop:build # Electron
### Testing
- `pnpm test:all` - Run all tests (parallel + sequential)
- `pnpm test:parallel` - Run tests that can run in parallel
- `pnpm test:sequential` - Run tests that must run sequentially (server, ckeditor5-mermaid, ckeditor5-math)
- `pnpm coverage` - Generate coverage reports
# Test
pnpm test:all # All tests (parallel + sequential)
pnpm test:parallel # Client + most package tests
pnpm test:sequential # Server, ckeditor5-mermaid, ckeditor5-math (shared DB)
pnpm --filter server test # Single package tests
pnpm coverage # Coverage reports
## Architecture Overview
# Lint & Format
pnpm dev:linter-check # ESLint check
pnpm dev:linter-fix # ESLint fix
pnpm dev:format-check # Format check (stricter stylistic rules)
pnpm dev:format-fix # Format fix
pnpm typecheck # TypeScript type check across all projects
```
### Monorepo Structure
- **apps/**: Runnable applications
- `client/` - Frontend application (shared by server and desktop)
- `server/` - Node.js server with web interface
- `desktop/` - Electron desktop application
- `web-clipper/` - Browser extension for saving web content
- Additional tools: `db-compare`, `dump-db`, `edit-docs`
**Running a single test file**: `pnpm --filter server test spec/etapi/search.spec.ts`
- **packages/**: Shared libraries
- `commons/` - Shared interfaces and utilities
- `ckeditor5/` - Custom rich text editor with Trilium-specific plugins
- `codemirror/` - Code editor customizations
- `highlightjs/` - Syntax highlighting
- Custom CKEditor plugins: `ckeditor5-admonition`, `ckeditor5-footnotes`, `ckeditor5-math`, `ckeditor5-mermaid`
## Main Applications
### Core Architecture Patterns
The four main apps share `packages/trilium-core/` for business logic but differ in runtime:
#### Three-Layer Cache System
- **Becca** (Backend Cache): Server-side entity cache (`apps/server/src/becca/`)
- **Froca** (Frontend Cache): Client-side mirror of backend data (`apps/client/src/services/froca.ts`)
- **Shaca** (Share Cache): Optimized cache for shared/published notes (`apps/server/src/share/`)
- **client** (`apps/client/`): Preact frontend with jQuery widget system. Shared UI layer used by both server and desktop.
- **server** (`apps/server/`): Node.js backend (Express, better-sqlite3). Serves the client and provides REST/WebSocket APIs.
- **desktop** (`apps/desktop/`): Electron wrapper around server + client, running both in a single process.
- **standalone** (`apps/client-standalone/` + `apps/standalone-desktop/`): Runs the entire stack in the browser — server logic compiled to WASM via sql.js, executed in a service worker. No Node.js dependency at runtime.
#### Entity System
Core entities are defined in `apps/server/src/becca/entities/`:
- `BNote` - Notes with content and metadata
- `BBranch` - Hierarchical relationships between notes (allows multiple parents)
- `BAttribute` - Key-value metadata attached to notes
- `BRevision` - Note version history
- `BOption` - Application configuration
## Monorepo Structure
#### Widget-Based UI
Frontend uses a widget system (`apps/client/src/widgets/`):
- `BasicWidget` - Base class for all UI components
- `NoteContextAwareWidget` - Widgets that respond to note changes
- `RightPanelWidget` - Widgets displayed in the right panel
```
apps/
client/ # Preact frontend (shared by server, desktop, standalone)
server/ # Node.js backend (Express, better-sqlite3)
desktop/ # Electron (bundles server + client)
client-standalone/ # Standalone client (WASM + service workers, no Node.js)
standalone-desktop/ # Standalone desktop variant
server-e2e/ # Playwright E2E tests for server
web-clipper/ # Browser extension
website/ # Project website
db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/
packages/
trilium-core/ # Core business logic: entities, services, SQL, sync
commons/ # Shared interfaces and utilities
ckeditor5/ # Custom rich text editor bundle
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
share-theme/ # Theme for shared/published notes
ckeditor5-admonition/, ckeditor5-footnotes/, ckeditor5-math/, ckeditor5-mermaid/
ckeditor5-keyboard-marker/, express-partial-content/, pdfjs-viewer/, splitjs/
turndown-plugin-gfm/
```
Use `pnpm --filter <package-name> <command>` to run commands in specific packages.
## Core Architecture
### Three-Layer Cache System
All data access goes through cache layers — never bypass with direct DB queries:
- **Becca** (`packages/trilium-core/src/becca/`): Server-side entity cache. Access via `becca.notes[noteId]`.
- **Froca** (`apps/client/src/services/froca.ts`): Client-side mirror synced via WebSocket. Access via `froca.getNote()`.
- **Shaca** (`apps/server/src/share/`): Optimized cache for shared/published notes.
**Critical**: Always use cache methods, not direct DB writes. Cache methods create `EntityChange` records needed for synchronization.
### Entity System
Core entities live in `packages/trilium-core/src/becca/entities/` (not `apps/server/`):
- `BNote` — Notes with content and metadata
- `BBranch` — Multi-parent tree relationships (cloning supported)
- `BAttribute` — Key-value metadata (labels and relations)
- `BRevision` — Version history
- `BOption` — Application configuration
- `BBlob` — Binary content storage
Entities extend `AbstractBeccaEntity<T>` with built-in change tracking, hash generation, and date management.
### Entity Change & Sync Protocol
Every entity modification creates an `EntityChange` record driving sync:
1. Login with HMAC authentication (document secret + timestamp)
2. Push changes → Pull changes → Push again (conflict resolution)
3. Content hash verification with retry loop
Sync services: `packages/trilium-core/src/services/sync.ts`, `syncMutexService`, `syncUpdateService`.
### Widget-Based UI
Frontend widgets in `apps/client/src/widgets/`:
- `BasicWidget` / `TypedBasicWidget` — Base classes (jQuery `this.$widget` for DOM)
- `NoteContextAwareWidget` — Responds to note changes
- `RightPanelWidget` — Sidebar widgets with position ordering
- Type-specific widgets in `type_widgets/` directory
#### API Architecture
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
- **WebSocket**: Real-time synchronization (`apps/server/src/services/ws.ts`)
**Widget lifecycle**: `doRenderBody()` for initial render, `refreshWithNote()` for note changes, `entitiesReloadedEvent({loadResults})` for entity updates. Uses jQuery — don't mix React patterns.
### Key Files for Understanding Architecture
Fluent builder pattern: `.child()`, `.class()`, `.css()` chaining with position-based ordering.
1. **Application Entry Points**:
- `apps/server/src/main.ts` - Server startup
- `apps/client/src/desktop.ts` - Client initialization
### API Architecture
2. **Core Services**:
- `apps/server/src/becca/becca.ts` - Backend data management
- `apps/client/src/services/froca.ts` - Frontend data synchronization
- `apps/server/src/services/backend_script_api.ts` - Scripting API
- **Internal API** (`apps/server/src/routes/api/`): REST endpoints, trusts frontend
- **ETAPI** (`apps/server/src/etapi/`): External API with basic auth tokens — maintain backwards compatibility
- **WebSocket** (`apps/server/src/services/ws.ts`): Real-time sync
3. **Database Schema**:
- `apps/server/src/assets/db/schema.sql` - Core database structure
### Platform Abstraction
4. **Configuration**:
- `package.json` - Project dependencies and scripts
`packages/trilium-core/src/services/platform.ts` defines `PlatformProvider` interface with implementations in `apps/desktop/`, `apps/server/`, and `apps/client-standalone/`. Singleton via `initPlatform()`/`getPlatform()`.
## Note Types and Features
**PlatformProvider** provides:
- `crash(message)` — Platform-specific fatal error handling
- `getEnv(key)` — Environment variable access (server/desktop use `process.env`, standalone maps URL query params like `?safeMode``TRILIUM_SAFE_MODE`)
- `isElectron`, `isMac`, `isWindows` — Platform detection flags
Trilium supports multiple note types, each with specialized widgets:
- **Text**: Rich text with CKEditor5 (markdown import/export)
- **Code**: Syntax-highlighted code editing with CodeMirror
- **File**: Binary file attachments
- **Image**: Image display with editing capabilities
- **Canvas**: Drawing/diagramming with Excalidraw
- **Mermaid**: Diagram generation
- **Relation Map**: Visual note relationship mapping
- **Web View**: Embedded web pages
- **Doc/Book**: Hierarchical documentation structure
**Critical rules for `trilium-core`**:
- **No `process.env` in core** — use `getPlatform().getEnv()` instead (not available in standalone/browser)
- **No `import path from "path"` in core** — Node's `path` module is externalized in browser builds. Use `packages/trilium-core/src/services/utils/path.ts` for `extname()`/`basename()` equivalents
- **No Node.js built-in modules in core** — core runs in both Node.js and the browser (standalone). Use platform-agnostic alternatives or platform providers
- **Platform detection via functions** — `isElectron()`, `isMac()`, `isWindows()` from `utils/index.ts` are functions (not constants) that call `getPlatform()`. They can only be called after `initializeCore()`, not at module top-level. If used in static definitions, wrap in a closure: `value: () => isWindows() ? "0.9" : "1.0"`
- **Barrel import caution** — `import { x } from "@triliumnext/core"` loads ALL core exports. Early-loading modules like `config.ts` should import specific subpaths (e.g. `@triliumnext/core/src/services/utils/index`) to avoid circular dependencies or initialization ordering issues
- **Electron IPC** — In desktop mode, client API calls use Electron IPC (not HTTP). The IPC handler in `apps/server/src/routes/electron.ts` must be registered via `utils.isElectron` from the **server's** utils (which correctly checks `process.versions["electron"]`), not from core's utils
## Development Guidelines
### Database
### Testing Strategy
- Server tests run sequentially due to shared database
- Client tests can run in parallel
- E2E tests use Playwright for both server and desktop apps
- Build validation tests check artifact integrity
SQLite via `better-sqlite3`. SQL abstraction in `packages/trilium-core/src/services/sql/` with `DatabaseProvider` interface, prepared statement caching, and transaction support.
### Scripting System
Trilium provides powerful user scripting capabilities:
- Frontend scripts run in browser context
- Backend scripts run in Node.js context with full API access
- Script API documentation available in `docs/Script API/`
- Schema: `apps/server/src/assets/db/schema.sql`
- Migrations: `apps/server/src/migrations/YYMMDD_HHMM__description.sql`
### Internationalization
- Translation files in `apps/client/src/translations/`
- Supported languages: English, German, Spanish, French, Romanian, Chinese
- **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`
### Attribute Inheritance
### 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)
Three inheritance mechanisms:
1. **Standard**: `note.getInheritableAttributes()` walks parent tree
2. **Child prefix**: `child:label` on parent copies to children
3. **Template relation**: `#template=noteNoteId` includes template's inheritable attributes
### Security Considerations
- Per-note encryption with granular protected sessions
- CSRF protection for API endpoints
- OpenID and TOTP authentication support
- Sanitization of user-generated content
Use `note.getOwnedAttribute()` for direct, `note.getAttribute()` for inherited.
### Client-Side API Restrictions
- **Do not use `crypto.randomUUID()`** or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS
- Use `randomString()` from `apps/client/src/services/utils.ts` for generating IDs instead
## Important Patterns
### Shared Types Policy
- Types shared between client and server belong in `@triliumnext/commons` (`packages/commons/src/lib/`)
- Import shared types directly from `@triliumnext/commons` - do not re-export them from app-specific modules
- Keep app-specific types (e.g., `LlmProvider` for server, `StreamCallbacks` for client) in their respective apps
- **Protected notes**: Check `note.isContentAvailable()` before accessing content; use `note.getTitleOrProtected()` for safe title access
- **Long operations**: Use `TaskContext` for progress reporting via WebSocket
- **Event system** (`packages/trilium-core/src/services/events.ts`): Events emitted in order (notes → branches → attributes) during load for referential integrity
- **Search**: Expression-based, scoring happens in-memory — cannot add SQL-level LIMIT/OFFSET without losing scoring
- **Widget cleanup**: Unsubscribe from events in `cleanup()`/`doDestroy()` to prevent memory leaks
## Common Development Tasks
## Code Style
### Adding New Note Types
1. Create widget in `apps/client/src/widgets/type_widgets/`
2. Register in `apps/client/src/services/note_types.ts`
3. Add backend handling in `apps/server/src/services/notes.ts`
- 4-space indentation, semicolons always required
- Double quotes (enforced by format config)
- Max line length: 100 characters
- Unix line endings
- Import sorting via `eslint-plugin-simple-import-sort`
### Extending Search
- Search expressions handled in `apps/server/src/services/search/`
- Add new search operators in search context files
## Testing
### Custom CKEditor Plugins
- Create new package in `packages/` following existing plugin structure
- Register in `packages/ckeditor5/src/plugins.ts`
- **Server tests** (`apps/server/spec/`): Vitest, must run sequentially (shared DB), forks pool, max 6 workers
- **Client tests** (`apps/client/src/`): Vitest with happy-dom environment, can run in parallel
- **E2E tests** (`apps/server-e2e/`): Playwright, Chromium, server started automatically on port 8082
- **ETAPI tests** (`apps/server/spec/etapi/`): External API contract tests
### 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`.
## Documentation
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`
- `docs/Script API/` — Auto-generated, never edit directly
- `docs/User Guide/` — Edit via `pnpm edit-docs:edit-docs`, not manually
- `docs/Developer Guide/` and `docs/Release Notes/` — Safe for direct Markdown editing
### 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
## Key Entry Points
### 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.
1. Add the tool definition in the appropriate module (`note_tools.ts`, `attribute_tools.ts`, `attachment_tools.ts`, `hierarchy_tools.ts`) or create a new module
2. Each tool needs: `description`, `inputSchema` (Zod), `execute` function, and optionally `mutates: true` for write operations
3. If creating a new module, wrap tools in `defineTools({...})` and add the registry to `allToolRegistries` in `tools/index.ts`
4. Add a client-side friendly name in `apps/client/src/translations/en/translation.json` under `llm.tools.<tool_name>` — use **imperative tense** (e.g. "Search notes", "Create note", "Get attributes"), not present continuous
5. Use ETAPI (`apps/server/src/etapi/`) as inspiration for what fields to expose, but **do not import ETAPI mappers** — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer
### Updating PDF.js
1. Update `pdfjs-dist` version in `packages/pdfjs-viewer/package.json`
2. Run `npx tsx scripts/update-viewer.ts` from that directory
3. Run `pnpm build` to verify success
4. Commit all changes including updated viewer files
### Database Migrations
- Add migration scripts in `apps/server/src/migrations/`
- Update schema in `apps/server/src/assets/db/schema.sql`
### Server-Side Static Assets
- Static assets (templates, SQL, translations, etc.) go in `apps/server/src/assets/`
- Access them at runtime via `RESOURCE_DIR` from `apps/server/src/services/resource_dir.ts` (e.g. `path.join(RESOURCE_DIR, "llm", "skills", "file.md")`)
- **Do not use `import.meta.url`/`fileURLToPath`** to resolve file paths — the server is bundled into CJS for production, so `import.meta.url` will not point to the source directory
- **Do not use `__dirname` with relative paths** from source files — after bundling, `__dirname` points to the bundle output, not the original source tree
## MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at `http://localhost:8080/mcp`, configured in `.mcp.json`
- The MCP server is **only available when the Trilium server is running** (`pnpm run server:start`)
- It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
## Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds
- `apps/server/src/main.ts` — Server startup
- `apps/client/src/desktop.ts` — Client initialization
- `packages/trilium-core/src/becca/becca.ts` — Backend data management
- `apps/client/src/services/froca.ts` — Frontend cache
- `apps/server/src/routes/routes.ts` — API route registration
- `packages/trilium-core/src/services/sql/sql.ts` — Database abstraction

View File

@@ -2,87 +2,13 @@
## Supported Versions
Only the latest stable minor release receives security fixes.
In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones).
For example, if the latest stable version is 0.92.3 and the latest beta is 0.93.0-beta, then only the 0.92.x line will receive security patches. Older versions (like 0.91.x) will not receive fixes.
So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes.
This policy may be altered on a case-by-case basis for critical vulnerabilities.
Description above is a general rule and may be altered on case by case basis.
## Reporting a Vulnerability
**Please report all security vulnerabilities through [GitHub Security Advisories](https://github.com/TriliumNext/Notes/security/advisories/new).**
We do not accept security reports via email, public issues, or other channels. GitHub Security Advisories allows us to:
- Discuss and triage vulnerabilities privately
- Coordinate fixes before public disclosure
- Credit reporters appropriately
- Publish advisories with CVE identifiers
### What to Include
When reporting, please provide:
- A clear description of the vulnerability
- Steps to reproduce or proof-of-concept
- Affected versions (if known)
- Potential impact assessment
- Any suggested mitigations or fixes
### Response Timeline
- **Initial response**: Within 7 days
- **Triage decision**: Within 14 days
- **Fix timeline**: Depends on severity and complexity
## Scope
### In Scope
- Remote code execution
- Authentication/authorization bypass
- Cross-site scripting (XSS) that affects other users
- SQL injection
- Path traversal
- Sensitive data exposure
- Privilege escalation
### Out of Scope (Won't Fix)
The following are considered out of scope or accepted risks:
#### Self-XSS / Self-Injection
Trilium is a personal knowledge base where users have full control over their own data. Users can intentionally create notes containing scripts, HTML, or other executable content. This is by design - Trilium's scripting system allows users to extend functionality with custom JavaScript.
Vulnerabilities that require a user to inject malicious content into their own notes and then view it themselves are not considered security issues.
#### Electron Architecture (nodeIntegration)
Trilium's desktop application runs with `nodeIntegration: true` to enable its powerful scripting features. This is an intentional design decision, similar to VS Code extensions having full system access. We mitigate risks by:
- Sanitizing content at input boundaries
- Fixing specific XSS vectors as they're discovered
- Using Electron fuses to prevent external abuse
#### Authenticated User Actions
Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities.
#### Denial of Service via Resource Exhaustion
Creating extremely large notes or performing many operations is expected user behavior in a note-taking application.
#### Missing Security Headers on Non-Sensitive Endpoints
We implement security headers where they provide meaningful protection, but may omit them on endpoints where they provide no practical benefit.
## Coordinated Disclosure
We follow a coordinated disclosure process:
1. **Report received** - We acknowledge receipt and begin triage
2. **Fix developed** - We develop and test a fix privately
3. **Release prepared** - Security release is prepared with vague changelog
4. **Users notified** - Release is published, users encouraged to upgrade
5. **Advisory published** - After reasonable upgrade window (typically 2-4 weeks), full advisory is published
We appreciate reporters allowing us time to fix issues before public disclosure. We aim to credit all reporters in published advisories unless they prefer to remain anonymous.
## Security Updates
Security fixes are released as patch versions (e.g., 0.92.1 → 0.92.2) to minimize upgrade friction. We recommend all users keep their installations up to date.
Subscribe to GitHub releases or watch the repository to receive notifications of new releases.
* For low severity vulnerabilities, they can be reported as GitHub issues.
* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories).

View File

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

View File

@@ -0,0 +1,4 @@
# The development license key for premium CKEditor features.
# Note: This key must only be used for the Trilium Notes project.
VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3ODcyNzA0MDAsImp0aSI6IjkyMWE1MWNlLTliNDMtNGRlMC1iOTQwLTc5ZjM2MDBkYjg1NyIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOiJ0cmlsaXVtIiwiZmVhdHVyZXMiOlsiVFJJTElVTSJdLCJ2YyI6ImU4YzRhMjBkIn0.hny77p-U4-jTkoqbwPytrEar5ylGCWBN7Ez3SlB8i6_mJCBIeCSTOlVQk_JMiOEq3AGykUMHzWXzjdMFwgniOw
VITE_CKEDITOR_ENABLE_INSPECTOR=false

View File

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

View File

@@ -0,0 +1,87 @@
{
"name": "@triliumnext/client-standalone",
"version": "0.102.1",
"description": "Standalone client for TriliumNext with SQLite WASM backend",
"private": true,
"license": "AGPL-3.0-only",
"scripts": {
"build": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite build",
"dev": "vite dev",
"test": "vitest",
"start-prod": "pnpm build && pnpm http-server dist -p 8888",
"coverage": "vitest --coverage"
},
"dependencies": {
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.8.2",
"@sqlite.org/sqlite-wasm": "3.51.1-build2",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/core": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@zumer/snapdom": "2.6.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"clsx": "2.1.1",
"color": "5.0.3",
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.2",
"globals": "17.4.0",
"i18next": "25.10.10",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"js-sha1": "0.7.0",
"js-sha256": "0.11.1",
"js-sha512": "0.9.0",
"jsplumb": "2.15.6",
"katex": "0.16.43",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.5",
"mermaid": "11.13.0",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"preact": "10.29.0",
"react-i18next": "16.6.6",
"react-window": "2.2.7",
"reveal.js": "6.0.0",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.4.0",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@preact/preset-vite": "2.10.2",
"@types/bootstrap": "5.2.10",
"@types/jquery": "4.0.0",
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "14.0.0",
"cross-env": "7.0.3",
"happy-dom": "20.8.8",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.4.0"
}
}

View File

@@ -0,0 +1,3 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -0,0 +1,20 @@
{
"name": "Trilium Notes",
"short_name": "Trilium",
"description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.",
"theme_color": "#333333",
"background_color": "#1F1F1F",
"display": "standalone",
"scope": "/",
"start_url": "/",
"display_override": [
"window-controls-overlay"
],
"icons": [
{
"src": "assets/icon.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@@ -0,0 +1,2 @@
// Re-export desktop from client
export * from "../../client/src/desktop";

View File

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

View File

@@ -0,0 +1,283 @@
/**
* Browser-compatible router that mimics Express routing patterns.
* Supports path parameters (e.g., /api/notes/:noteId) and query strings.
*/
import { getContext, routes } from "@triliumnext/core";
export interface BrowserRequest {
method: string;
url: string;
path: string;
params: Record<string, string>;
query: Record<string, string | undefined>;
headers?: Record<string, string>;
body?: unknown;
}
export interface BrowserResponse {
status: number;
headers: Record<string, string>;
body: ArrayBuffer | null;
}
export type RouteHandler = (req: BrowserRequest) => unknown | Promise<unknown>;
interface Route {
method: string;
pattern: RegExp;
paramNames: string[];
handler: RouteHandler;
}
/**
* Symbol used to mark a result as an already-formatted response,
* so that formatResult passes it through without JSON-serializing.
* Must match the symbol exported from browser_routes.ts.
*/
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
const encoder = new TextEncoder();
/**
* Convert an Express-style path pattern to a RegExp.
* Supports :param syntax for path parameters.
*
* Examples:
* /api/notes/:noteId -> /^\/api\/notes\/([^\/]+)$/
* /api/notes/:noteId/revisions -> /^\/api\/notes\/([^\/]+)\/revisions$/
*/
function pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
const paramNames: string[] = [];
// Escape special regex characters except for :param patterns
const regexPattern = path
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
});
return {
pattern: new RegExp(`^${regexPattern}$`),
paramNames
};
}
/**
* Parse query string into an object.
*/
function parseQuery(search: string): Record<string, string | undefined> {
const query: Record<string, string | undefined> = {};
if (!search || search === '?') return query;
const params = new URLSearchParams(search);
for (const [key, value] of params) {
query[key] = value;
}
return query;
}
/**
* Convert a result to a JSON response.
*/
function jsonResponse(obj: unknown, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const parsedObj = routes.convertEntitiesToPojo(obj);
const body = encoder.encode(JSON.stringify(parsedObj)).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "application/json; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Convert a string to a text response.
*/
function textResponse(text: string, status = 200, extraHeaders: Record<string, string> = {}): BrowserResponse {
const body = encoder.encode(text).buffer as ArrayBuffer;
return {
status,
headers: { "content-type": "text/plain; charset=utf-8", ...extraHeaders },
body
};
}
/**
* Browser router class that handles route registration and dispatching.
*/
export class BrowserRouter {
private routes: Route[] = [];
/**
* Register a route handler.
*/
register(method: string, path: string, handler: RouteHandler): void {
const { pattern, paramNames } = pathToRegex(path);
this.routes.push({
method: method.toUpperCase(),
pattern,
paramNames,
handler
});
}
/**
* Convenience methods for common HTTP methods.
*/
get(path: string, handler: RouteHandler): void {
this.register('GET', path, handler);
}
post(path: string, handler: RouteHandler): void {
this.register('POST', path, handler);
}
put(path: string, handler: RouteHandler): void {
this.register('PUT', path, handler);
}
patch(path: string, handler: RouteHandler): void {
this.register('PATCH', path, handler);
}
delete(path: string, handler: RouteHandler): void {
this.register('DELETE', path, handler);
}
/**
* Dispatch a request to the appropriate handler.
*/
async dispatch(method: string, urlString: string, body?: unknown, headers?: Record<string, string>): Promise<BrowserResponse> {
const url = new URL(urlString);
const path = url.pathname;
const query = parseQuery(url.search);
const upperMethod = method.toUpperCase();
// Parse JSON body if it's an ArrayBuffer and content-type suggests JSON
let parsedBody = body;
if (body instanceof ArrayBuffer && headers) {
const contentType = headers['content-type'] || headers['Content-Type'] || '';
if (contentType.includes('application/json')) {
try {
const text = new TextDecoder().decode(body);
if (text.trim()) {
parsedBody = JSON.parse(text);
}
} catch (e) {
console.warn('[Router] Failed to parse JSON body:', e);
// Keep original body if JSON parsing fails
parsedBody = body;
}
}
}
// Find matching route
for (const route of this.routes) {
if (route.method !== upperMethod) continue;
const match = path.match(route.pattern);
if (!match) continue;
// Extract path parameters
const params: Record<string, string> = {};
for (let i = 0; i < route.paramNames.length; i++) {
params[route.paramNames[i]] = decodeURIComponent(match[i + 1]);
}
const request: BrowserRequest = {
method: upperMethod,
url: urlString,
path,
params,
query,
headers: headers ?? {},
body: parsedBody
};
try {
const result = await getContext().init(async () => await route.handler(request));
return this.formatResult(result);
} catch (error) {
return this.formatError(error, `Error handling ${method} ${path}`);
}
}
// No route matched
return textResponse(`Not found: ${method} ${path}`, 404);
}
/**
* Format a handler result into a response.
* Follows the same patterns as the server's apiResultHandler.
*/
private formatResult(result: unknown): BrowserResponse {
// Handle raw responses (e.g. from image routes that write directly to res)
if (result && typeof result === 'object' && RAW_RESPONSE in result) {
const raw = result as unknown as { status: number; headers: Record<string, string>; body: unknown };
let body: ArrayBuffer | null = null;
if (raw.body instanceof ArrayBuffer) {
body = raw.body;
} else if (raw.body instanceof Uint8Array) {
body = raw.body.buffer as ArrayBuffer;
} else if (typeof raw.body === 'string') {
body = encoder.encode(raw.body).buffer as ArrayBuffer;
}
return {
status: raw.status,
headers: raw.headers,
body
};
}
// Handle [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
return jsonResponse(response, statusCode);
}
// Handle undefined (no content) - 204 should have no body
if (result === undefined) {
return {
status: 204,
headers: {},
body: null
};
}
// Default: JSON response with 200
return jsonResponse(result, 200);
}
/**
* Format an error into a response.
*/
private formatError(error: unknown, context: string): BrowserResponse {
console.error('[Router] Handler error:', context, error);
// Check for known error types
if (error && typeof error === 'object') {
const err = error as { constructor?: { name?: string }; message?: string };
if (err.constructor?.name === 'NotFoundError') {
return jsonResponse({ message: err.message || 'Not found' }, 404);
}
if (err.constructor?.name === 'ValidationError') {
return jsonResponse({ message: err.message || 'Validation error' }, 400);
}
}
// Generic error
const message = error instanceof Error ? error.message : String(error);
return jsonResponse({ message }, 500);
}
}
/**
* Create a new router instance.
*/
export function createRouter(): BrowserRouter {
return new BrowserRouter();
}

View File

@@ -0,0 +1,284 @@
/**
* Browser route definitions.
* This integrates with the shared route builder from @triliumnext/core.
*/
import { BootstrapDefinition } from '@triliumnext/commons';
import { entity_changes, getContext, getPlatform, getSharedBootstrapItems, getSql, routes, sql_init } from '@triliumnext/core';
import packageJson from '../../package.json' with { type: 'json' };
import { type BrowserRequest, BrowserRouter } from './browser_router';
/** Minimal response object used by apiResultHandler to capture the processed result. */
interface ResultHandlerResponse {
headers: Record<string, string>;
result: unknown;
setHeader(name: string, value: string): void;
}
/**
* Symbol used to mark a result as an already-formatted BrowserResponse,
* so that BrowserRouter.formatResult passes it through without JSON-serializing.
* Uses Symbol.for() so the same symbol is shared across modules.
*/
const RAW_RESPONSE = Symbol.for('RAW_RESPONSE');
type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
/**
* Creates an Express-like request object from a BrowserRequest.
*/
function toExpressLikeReq(req: BrowserRequest) {
return {
params: req.params,
query: req.query,
body: req.body,
headers: req.headers ?? {},
method: req.method,
get originalUrl() { return req.url; }
};
}
/**
* Extracts context headers from the request and sets them in the execution context,
* mirroring what the server does in route_api.ts.
*/
function setContextFromHeaders(req: BrowserRequest) {
const headers = req.headers ?? {};
const ctx = getContext();
ctx.set("componentId", headers["trilium-component-id"]);
ctx.set("localNowDateTime", headers["trilium-local-now-datetime"]);
ctx.set("hoistedNoteId", headers["trilium-hoisted-note-id"] || "root");
}
/**
* Wraps a core route handler to work with the BrowserRouter.
* Core handlers expect an Express-like request object with params, query, and body.
* Each request is wrapped in an execution context (like cls.init() on the server)
* to ensure entity change tracking works correctly.
*/
function wrapHandler(handler: (req: any) => unknown, transactional: boolean) {
return (req: BrowserRequest) => {
return getContext().init(() => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
if (transactional) {
return getSql().transactional(() => handler(expressLikeReq));
}
return handler(expressLikeReq);
});
};
}
/**
* Creates an apiRoute function compatible with buildSharedApiRoutes.
* This bridges the core's route registration to the BrowserRouter.
*/
function createApiRoute(router: BrowserRouter, transactional: boolean) {
return (method: HttpMethod, path: string, handler: (req: any) => unknown) => {
router.register(method, path, wrapHandler(handler, transactional));
};
}
/**
* Low-level route registration matching the server's `route()` signature:
* route(method, path, middleware[], handler, resultHandler)
*
* In standalone mode:
* - Middleware (e.g. checkApiAuth) is skipped — there's no authentication.
* - The resultHandler is applied to post-process the result (entity conversion, status codes).
*/
function createRoute(router: BrowserRouter) {
return (method: HttpMethod, path: string, _middleware: any[], handler: (req: any, res: any) => unknown, resultHandler?: ((req: any, res: any, result: unknown) => unknown) | null) => {
router.register(method, path, (req: BrowserRequest) => {
return getContext().init(() => {
setContextFromHeaders(req);
const expressLikeReq = toExpressLikeReq(req);
const mockRes = createMockExpressResponse();
const result = getSql().transactional(() => handler(expressLikeReq, mockRes));
// If the handler used the mock response (e.g. image routes that call res.send()),
// return it as a raw response so BrowserRouter doesn't JSON-serialize it.
if (mockRes._used) {
return {
[RAW_RESPONSE]: true as const,
status: mockRes._status,
headers: mockRes._headers,
body: mockRes._body
};
}
if (resultHandler) {
// Create a minimal response object that captures what apiResultHandler sets.
const res = createResultHandlerResponse();
resultHandler(expressLikeReq, res, result);
return res.result;
}
return result;
});
});
};
}
/**
* Creates a mock Express response object that captures calls to set(), send(), sendStatus(), etc.
* Used for route handlers (like image routes) that write directly to the response.
*/
function createMockExpressResponse() {
const res = {
_used: false,
_status: 200,
_headers: {} as Record<string, string>,
_body: null as unknown,
set(name: string, value: string) {
res._headers[name] = value;
return res;
},
setHeader(name: string, value: string) {
res._headers[name] = value;
return res;
},
status(code: number) {
res._status = code;
return res;
},
send(body: unknown) {
res._used = true;
res._body = body;
return res;
},
sendStatus(code: number) {
res._used = true;
res._status = code;
return res;
}
};
return res;
}
/**
* Standalone apiResultHandler matching the server's behavior:
* - Converts Becca entities to POJOs
* - Handles [statusCode, response] tuple format
* - Sets trilium-max-entity-change-id (captured in response headers)
*/
function apiResultHandler(_req: any, res: ResultHandlerResponse, result: unknown) {
res.headers["trilium-max-entity-change-id"] = String(entity_changes.getMaxEntityChangeId());
result = routes.convertEntitiesToPojo(result);
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [_statusCode, response] = result;
res.result = response;
} else if (result === undefined) {
res.result = "";
} else {
res.result = result;
}
}
/**
* No-op middleware stubs for standalone mode.
*
* In a browser context there is no network authentication, rate limiting,
* or multi-user access, so all auth/rate-limit middleware is a no-op.
*
* `checkAppNotInitialized` still guards setup routes: if the database is
* already initialised the middleware throws so the route handler is never
* reached (mirrors the server behaviour).
*/
function noopMiddleware() {
// No-op.
}
function checkAppNotInitialized() {
if (sql_init.isDbInitialized()) {
throw new Error("App already initialized.");
}
}
/**
* Creates a minimal response-like object for the apiResultHandler.
*/
function createResultHandlerResponse(): ResultHandlerResponse {
return {
headers: {},
result: undefined,
setHeader(name: string, value: string) {
this.headers[name] = value;
}
};
}
/**
* Register all API routes on the browser router using the shared builder.
*
* @param router - The browser router instance
*/
export function registerRoutes(router: BrowserRouter): void {
const apiRoute = createApiRoute(router, true);
routes.buildSharedApiRoutes({
route: createRoute(router),
asyncRoute: createRoute(router),
apiRoute,
asyncApiRoute: createApiRoute(router, false),
apiResultHandler,
checkApiAuth: noopMiddleware,
checkApiAuthOrElectron: noopMiddleware,
checkAppNotInitialized,
checkCredentials: noopMiddleware,
loginRateLimiter: noopMiddleware
});
apiRoute('get', '/bootstrap', bootstrapRoute);
// Dummy routes for compatibility.
apiRoute("get", "/api/script/widgets", () => []);
apiRoute("get", "/api/script/startup", () => []);
apiRoute("get", "/api/system-checks", () => ({ isCpuArchMismatch: false }));
}
function bootstrapRoute(): BootstrapDefinition {
const assetPath = ".";
const isDbInitialized = sql_init.isDbInitialized();
const commonItems = {
...getSharedBootstrapItems(assetPath, isDbInitialized),
isDev: import.meta.env.DEV,
isStandalone: true,
isMainWindow: true,
isElectron: false,
hasNativeTitleBar: false,
hasBackgroundEffects: false,
triliumVersion: packageJson.version,
device: false as const, // Let the client detect device type.
appPath: assetPath,
instanceName: "standalone",
TRILIUM_SAFE_MODE: !!getPlatform().getEnv("TRILIUM_SAFE_MODE")
};
if (!isDbInitialized) {
return {
...commonItems,
baseApiUrl: "../api/",
isProtectedSessionAvailable: false,
};
}
return {
...commonItems,
csrfToken: "dummy-csrf-token",
baseApiUrl: "../api/",
headingStyle: "plain",
layoutOrientation: "vertical",
platform: "web",
};
}
/**
* Create and configure a router with all routes registered.
*/
export function createConfiguredRouter(): BrowserRouter {
const router = new BrowserRouter();
registerRoutes(router);
return router;
}

View File

@@ -0,0 +1,77 @@
import { ExecutionContext } from "@triliumnext/core";
/**
* Browser execution context implementation.
*
* Handles per-request context isolation with support for fire-and-forget async operations
* using a context stack and grace-period cleanup to allow unawaited promises to complete.
*/
export default class BrowserExecutionContext implements ExecutionContext {
private contextStack: Map<string, any>[] = [];
private cleanupTimers = new WeakMap<Map<string, any>, ReturnType<typeof setTimeout>>();
private readonly CLEANUP_GRACE_PERIOD = 1000; // 1 second for fire-and-forget operations
private getCurrentContext(): Map<string, any> {
if (this.contextStack.length === 0) {
throw new Error("ExecutionContext not initialized");
}
return this.contextStack[this.contextStack.length - 1];
}
get<T = any>(key: string): T {
return this.getCurrentContext().get(key);
}
set(key: string, value: any): void {
this.getCurrentContext().set(key, value);
}
reset(): void {
this.contextStack = [];
}
init<T>(callback: () => T): T {
const context = new Map<string, any>();
this.contextStack.push(context);
// Cancel any pending cleanup timer for this context
const existingTimer = this.cleanupTimers.get(context);
if (existingTimer) {
clearTimeout(existingTimer);
this.cleanupTimers.delete(context);
}
try {
const result = callback();
// If the result is a Promise
if (result && typeof result === 'object' && 'then' in result && 'catch' in result) {
const promise = result as unknown as Promise<any>;
return promise.finally(() => {
this.scheduleContextCleanup(context);
}) as T;
} else {
// For synchronous results, schedule delayed cleanup to allow fire-and-forget operations
this.scheduleContextCleanup(context);
return result;
}
} catch (error) {
// Always clean up on error with grace period
this.scheduleContextCleanup(context);
throw error;
}
}
private scheduleContextCleanup(context: Map<string, any>): void {
const timer = setTimeout(() => {
// Remove from stack if still present
const index = this.contextStack.indexOf(context);
if (index !== -1) {
this.contextStack.splice(index, 1);
}
this.cleanupTimers.delete(context);
}, this.CLEANUP_GRACE_PERIOD);
this.cleanupTimers.set(context, timer);
}
}

View File

@@ -0,0 +1,158 @@
import type { CryptoProvider } from "@triliumnext/core";
import { sha1 } from "js-sha1";
import { sha256 } from "js-sha256";
import { sha512 } from "js-sha512";
interface Cipher {
update(data: Uint8Array): Uint8Array;
final(): Uint8Array;
}
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* Crypto provider for browser environments using the Web Crypto API.
*/
export default class BrowserCryptoProvider implements CryptoProvider {
createHash(algorithm: "sha1" | "sha512", content: string | Uint8Array): Uint8Array {
const data = typeof content === "string" ? content :
new TextDecoder().decode(content);
const hexHash = algorithm === "sha1" ? sha1(data) : sha512(data);
// Convert hex string to Uint8Array
const bytes = new Uint8Array(hexHash.length / 2);
for (let i = 0; i < hexHash.length; i += 2) {
bytes[i / 2] = parseInt(hexHash.substr(i, 2), 16);
}
return bytes;
}
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
// Web Crypto API doesn't support streaming cipher like Node.js
// We need to implement a wrapper that collects data and encrypts on final()
return new WebCryptoCipher(algorithm, key, iv, "encrypt");
}
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
return new WebCryptoCipher(algorithm, key, iv, "decrypt");
}
randomBytes(size: number): Uint8Array {
const bytes = new Uint8Array(size);
crypto.getRandomValues(bytes);
return bytes;
}
randomString(length: number): string {
const bytes = this.randomBytes(length);
let result = "";
for (let i = 0; i < length; i++) {
result += CHARS[bytes[i] % CHARS.length];
}
return result;
}
hmac(secret: string | Uint8Array, value: string | Uint8Array): string {
const secretStr = typeof secret === "string" ? secret : new TextDecoder().decode(secret);
const valueStr = typeof value === "string" ? value : new TextDecoder().decode(value);
// sha256.hmac returns hex, convert to base64 to match Node's behavior
const hexHash = sha256.hmac(secretStr, valueStr);
const bytes = new Uint8Array(hexHash.length / 2);
for (let i = 0; i < hexHash.length; i += 2) {
bytes[i / 2] = parseInt(hexHash.substr(i, 2), 16);
}
return btoa(String.fromCharCode(...bytes));
}
}
/**
* A cipher implementation that wraps Web Crypto API.
* Note: This buffers all data until final() is called, which differs from
* Node.js's streaming cipher behavior.
*/
class WebCryptoCipher implements Cipher {
private chunks: Uint8Array[] = [];
private algorithm: string;
private key: Uint8Array;
private iv: Uint8Array;
private mode: "encrypt" | "decrypt";
private finalized = false;
constructor(
algorithm: "aes-128-cbc",
key: Uint8Array,
iv: Uint8Array,
mode: "encrypt" | "decrypt"
) {
this.algorithm = algorithm;
this.key = key;
this.iv = iv;
this.mode = mode;
}
update(data: Uint8Array): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
// Buffer the data - Web Crypto doesn't support streaming
this.chunks.push(data);
// Return empty array since we process everything in final()
return new Uint8Array(0);
}
final(): Uint8Array {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
this.finalized = true;
// Web Crypto API is async, but we need sync behavior
// This is a fundamental limitation that requires architectural changes
// For now, throw an error directing users to use async methods
throw new Error(
"Synchronous cipher finalization not available in browser. " +
"The Web Crypto API is async-only. Use finalizeAsync() instead."
);
}
/**
* Async version that actually performs the encryption/decryption.
*/
async finalizeAsync(): Promise<Uint8Array> {
if (this.finalized) {
throw new Error("Cipher has already been finalized");
}
this.finalized = true;
// Concatenate all chunks
const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const data = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of this.chunks) {
data.set(chunk, offset);
offset += chunk.length;
}
// Copy key and iv to ensure they're plain ArrayBuffer-backed
const keyBuffer = new Uint8Array(this.key);
const ivBuffer = new Uint8Array(this.iv);
// Import the key
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBuffer,
{ name: "AES-CBC" },
false,
[this.mode]
);
// Perform encryption/decryption
const result = this.mode === "encrypt"
? await crypto.subtle.encrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data)
: await crypto.subtle.decrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data);
return new Uint8Array(result);
}
}

View File

@@ -0,0 +1,120 @@
import type { WebSocketMessage } from "@triliumnext/commons";
import type { ClientMessageHandler, MessageHandler,MessagingProvider } from "@triliumnext/core";
/**
* Messaging provider for browser Worker environments.
*
* This provider uses the Worker's postMessage API to communicate
* with the main thread. It's designed to be used inside a Web Worker
* that runs the core services.
*
* Message flow:
* - Outbound (worker → main): Uses self.postMessage() with type: "WS_MESSAGE"
* - Inbound (main → worker): Listens to onmessage for type: "WS_MESSAGE"
*/
export default class WorkerMessagingProvider implements MessagingProvider {
private messageHandlers: MessageHandler[] = [];
private clientMessageHandler?: ClientMessageHandler;
private isDisposed = false;
constructor() {
// Listen for incoming messages from the main thread
self.addEventListener("message", this.handleIncomingMessage);
}
private handleIncomingMessage = (event: MessageEvent) => {
if (this.isDisposed) return;
const { type, message } = event.data || {};
if (type === "WS_MESSAGE" && message) {
// Dispatch to the client message handler (used by ws.ts for log-error, log-info, ping)
if (this.clientMessageHandler) {
try {
this.clientMessageHandler("main-thread", message);
} catch (e) {
console.error("[WorkerMessagingProvider] Error in client message handler:", e);
}
}
// Dispatch to all registered handlers
for (const handler of this.messageHandlers) {
try {
handler(message as WebSocketMessage);
} catch (e) {
console.error("[WorkerMessagingProvider] Error in message handler:", e);
}
}
}
};
/**
* Send a message to all clients (in this case, the main thread).
* The main thread is responsible for further distribution if needed.
*/
sendMessageToAllClients(message: WebSocketMessage): void {
if (this.isDisposed) {
console.warn("[WorkerMessagingProvider] Cannot send message - provider is disposed");
return;
}
try {
self.postMessage({
type: "WS_MESSAGE",
message
});
} catch (e) {
console.error("[WorkerMessagingProvider] Error sending message:", e);
}
}
/**
* Send a message to a specific client.
* In worker context, there's only one client (the main thread), so clientId is ignored.
*/
sendMessageToClient(_clientId: string, message: WebSocketMessage): boolean {
if (this.isDisposed) {
return false;
}
this.sendMessageToAllClients(message);
return true;
}
/**
* Register a handler for incoming client messages.
*/
setClientMessageHandler(handler: ClientMessageHandler): void {
this.clientMessageHandler = handler;
}
/**
* Subscribe to incoming messages from the main thread.
*/
onMessage(handler: MessageHandler): () => void {
this.messageHandlers.push(handler);
return () => {
this.messageHandlers = this.messageHandlers.filter(h => h !== handler);
};
}
/**
* Get the number of connected "clients".
* In worker context, there's always exactly 1 client (the main thread).
*/
getClientCount(): number {
return this.isDisposed ? 0 : 1;
}
/**
* Clean up resources.
*/
dispose(): void {
if (this.isDisposed) return;
this.isDisposed = true;
self.removeEventListener("message", this.handleIncomingMessage);
this.messageHandlers = [];
}
}

View File

@@ -0,0 +1,36 @@
import type { PlatformProvider } from "@triliumnext/core";
/** Maps URL query parameter names to TRILIUM_ environment variable names. */
const QUERY_TO_ENV: Record<string, string> = {
"safeMode": "TRILIUM_SAFE_MODE",
"startNoteId": "TRILIUM_START_NOTE_ID",
};
export default class StandalonePlatformProvider implements PlatformProvider {
readonly isElectron = false;
readonly isMac = false;
readonly isWindows = false;
private envMap: Record<string, string> = {};
constructor(queryString: string) {
const params = new URLSearchParams(queryString);
for (const [queryKey, envKey] of Object.entries(QUERY_TO_ENV)) {
if (params.has(queryKey)) {
this.envMap[envKey] = params.get(queryKey) || "true";
}
}
}
crash(message: string): void {
console.error("[Standalone] FATAL:", message);
self.postMessage({
type: "FATAL_ERROR",
message
});
}
getEnv(key: string): string | undefined {
return this.envMap[key];
}
}

View File

@@ -0,0 +1,93 @@
import type { ExecOpts, RequestProvider } from "@triliumnext/core";
/**
* Fetch-based implementation of RequestProvider for browser environments.
*
* Uses the Fetch API instead of Node's http/https modules.
* Proxy support is not available in browsers, so the proxy option is ignored.
*/
export default class FetchRequestProvider implements RequestProvider {
async exec<T>(opts: ExecOpts): Promise<T> {
const paging = opts.paging || {
pageCount: 1,
pageIndex: 0,
requestId: "n/a"
};
const headers: Record<string, string> = {
"Content-Type": paging.pageCount === 1 ? "application/json" : "text/plain",
"pageCount": String(paging.pageCount),
"pageIndex": String(paging.pageIndex),
"requestId": paging.requestId
};
// Note: the Cookie header is a forbidden header in fetch —
// the browser manages cookies automatically via credentials: 'include'.
if (opts.auth?.password) {
headers["trilium-cred"] = btoa(`dummy:${opts.auth.password}`);
}
let body: string | undefined;
if (opts.body) {
body = typeof opts.body === "object" ? JSON.stringify(opts.body) : opts.body;
}
const controller = new AbortController();
const timeoutId = opts.timeout
? setTimeout(() => controller.abort(), opts.timeout)
: undefined;
try {
const response = await fetch(opts.url, {
method: opts.method,
headers,
body,
signal: controller.signal,
credentials: "include"
});
if ([200, 201, 204].includes(response.status)) {
const text = await response.text();
return text.trim() ? JSON.parse(text) : null;
}
const text = await response.text();
let errorMessage: string;
try {
const json = JSON.parse(text);
errorMessage = json?.message || "";
} catch {
errorMessage = text.substring(0, 100);
}
throw new Error(`${response.status} ${opts.method} ${opts.url}: ${errorMessage}`);
} catch (e: any) {
if (e.name === "AbortError") {
throw new Error(`${opts.method} ${opts.url} failed, error: timeout after ${opts.timeout}ms`);
}
if (e instanceof TypeError && e.message === "Failed to fetch") {
const isCrossOrigin = !opts.url.startsWith(location.origin);
if (isCrossOrigin) {
throw new Error(`Request to ${opts.url} was blocked. The server may not allow requests from this origin (CORS), or it may be unreachable.`);
}
throw new Error(`Request to ${opts.url} failed. The server may be unreachable.`);
}
throw e;
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
}
}
}
async getImage(imageUrl: string): Promise<ArrayBuffer> {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`${response.status} GET ${imageUrl} failed`);
}
return await response.arrayBuffer();
}
}

View File

@@ -0,0 +1,613 @@
import { type BindableValue, default as sqlite3InitModule } from "@sqlite.org/sqlite-wasm";
import type { DatabaseProvider, RunResult, Statement, Transaction } from "@triliumnext/core";
// Type definitions for SQLite WASM (the library doesn't export these directly)
type Sqlite3Module = Awaited<ReturnType<typeof sqlite3InitModule>>;
type Sqlite3Database = InstanceType<Sqlite3Module["oo1"]["DB"]>;
type Sqlite3PreparedStatement = ReturnType<Sqlite3Database["prepare"]>;
/**
* Wraps an SQLite WASM PreparedStatement to match the Statement interface
* expected by trilium-core.
*/
class WasmStatement implements Statement {
private isRawMode = false;
private isPluckMode = false;
private isFinalized = false;
constructor(
private stmt: Sqlite3PreparedStatement,
private db: Sqlite3Database,
private sqlite3: Sqlite3Module,
private sql: string
) {}
run(...params: unknown[]): RunResult {
if (this.isFinalized) {
throw new Error("Cannot call run() on finalized statement");
}
this.bindParams(params);
try {
// Use step() and then reset instead of stepFinalize()
// This allows the statement to be reused
this.stmt.step();
const changes = this.db.changes();
// Get the last insert row ID using the C API
const lastInsertRowid = this.db.pointer ? this.sqlite3.capi.sqlite3_last_insert_rowid(this.db.pointer) : 0;
this.stmt.reset();
return {
changes,
lastInsertRowid: typeof lastInsertRowid === "bigint" ? Number(lastInsertRowid) : lastInsertRowid
};
} catch (e) {
// Reset on error to allow reuse
this.stmt.reset();
throw e;
}
}
get(params: unknown): unknown {
if (this.isFinalized) {
throw new Error("Cannot call get() on finalized statement");
}
this.bindParams(Array.isArray(params) ? params : params !== undefined ? [params] : []);
try {
if (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value
const row = this.stmt.get([]);
return Array.isArray(row) && row.length > 0 ? row[0] : undefined;
}
return this.isRawMode ? this.stmt.get([]) : this.stmt.get({});
}
return undefined;
} finally {
this.stmt.reset();
}
}
all(...params: unknown[]): unknown[] {
if (this.isFinalized) {
throw new Error("Cannot call all() on finalized statement");
}
this.bindParams(params);
const results: unknown[] = [];
try {
while (this.stmt.step()) {
if (this.isPluckMode) {
// In pluck mode, return only the first column value for each row
const row = this.stmt.get([]);
if (Array.isArray(row) && row.length > 0) {
results.push(row[0]);
}
} else {
results.push(this.isRawMode ? this.stmt.get([]) : this.stmt.get({}));
}
}
return results;
} finally {
this.stmt.reset();
}
}
iterate(...params: unknown[]): IterableIterator<unknown> {
if (this.isFinalized) {
throw new Error("Cannot call iterate() on finalized statement");
}
this.bindParams(params);
const stmt = this.stmt;
const isRaw = this.isRawMode;
const isPluck = this.isPluckMode;
return {
[Symbol.iterator]() {
return this;
},
next(): IteratorResult<unknown> {
if (stmt.step()) {
if (isPluck) {
const row = stmt.get([]);
const value = Array.isArray(row) && row.length > 0 ? row[0] : undefined;
return { value, done: false };
}
return { value: isRaw ? stmt.get([]) : stmt.get({}), done: false };
}
stmt.reset();
return { value: undefined, done: true };
}
};
}
raw(toggleState?: boolean): this {
// In raw mode, rows are returned as arrays instead of objects
// If toggleState is undefined, enable raw mode (better-sqlite3 behavior)
this.isRawMode = toggleState !== undefined ? toggleState : true;
return this;
}
pluck(toggleState?: boolean): this {
// In pluck mode, only the first column of each row is returned
// If toggleState is undefined, enable pluck mode (better-sqlite3 behavior)
this.isPluckMode = toggleState !== undefined ? toggleState : true;
return this;
}
/**
* Detect the prefix used for a parameter name in the SQL query.
* SQLite supports @name, :name, and $name parameter styles.
* Returns the prefix character, or ':' as default if not found.
*/
private detectParamPrefix(paramName: string): string {
// Search for the parameter with each possible prefix
for (const prefix of [':', '@', '$']) {
// Use word boundary to avoid partial matches
const pattern = new RegExp(`\\${prefix}${paramName}(?![a-zA-Z0-9_])`);
if (pattern.test(this.sql)) {
return prefix;
}
}
// Default to ':' if not found (most common in Trilium)
return ':';
}
private bindParams(params: unknown[]): void {
this.stmt.clearBindings();
if (params.length === 0) {
return;
}
// Handle single object with named parameters
if (params.length === 1 && typeof params[0] === "object" && params[0] !== null && !Array.isArray(params[0])) {
const inputBindings = params[0] as { [paramName: string]: BindableValue };
// SQLite WASM expects parameter names to include the prefix (@ : or $)
// We detect the prefix used in the SQL for each parameter
const bindings: { [paramName: string]: BindableValue } = {};
for (const [key, value] of Object.entries(inputBindings)) {
// If the key already has a prefix, use it as-is
if (key.startsWith('@') || key.startsWith(':') || key.startsWith('$')) {
bindings[key] = value;
} else {
// Detect the prefix used in the SQL and apply it
const prefix = this.detectParamPrefix(key);
bindings[`${prefix}${key}`] = value;
}
}
this.stmt.bind(bindings);
} else {
// Handle positional parameters - flatten and cast to BindableValue[]
const flatParams = params.flat() as BindableValue[];
if (flatParams.length > 0) {
this.stmt.bind(flatParams);
}
}
}
finalize(): void {
if (!this.isFinalized) {
try {
this.stmt.finalize();
} catch (e) {
console.warn("Error finalizing SQLite statement:", e);
} finally {
this.isFinalized = true;
}
}
}
}
/**
* SQLite database provider for browser environments using SQLite WASM.
*
* This provider wraps the official @sqlite.org/sqlite-wasm package to provide
* a DatabaseProvider implementation compatible with trilium-core.
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm(); // Initialize SQLite WASM module
* provider.loadFromMemory(); // Open an in-memory database
* // or
* provider.loadFromBuffer(existingDbBuffer); // Load from existing data
* ```
*/
export default class BrowserSqlProvider implements DatabaseProvider {
private db?: Sqlite3Database;
private sqlite3?: Sqlite3Module;
private _inTransaction = false;
private initPromise?: Promise<void>;
private initError?: Error;
private statementCache: Map<string, WasmStatement> = new Map();
// OPFS state tracking
private opfsDbPath?: string;
/**
* Get the SQLite WASM module version info.
* Returns undefined if the module hasn't been initialized yet.
*/
get version(): { libVersion: string; sourceId: string } | undefined {
return this.sqlite3?.version;
}
/**
* Initialize the SQLite WASM module.
* This must be called before using any database operations.
* Safe to call multiple times - subsequent calls return the same promise.
*
* @returns A promise that resolves when the module is initialized
* @throws Error if initialization fails
*/
async initWasm(): Promise<void> {
// Return existing promise if already initializing/initialized
if (this.initPromise) {
return this.initPromise;
}
// Fail fast if we already tried and failed
if (this.initError) {
throw this.initError;
}
this.initPromise = this.doInitWasm();
return this.initPromise;
}
private async doInitWasm(): Promise<void> {
try {
console.log("[BrowserSqlProvider] Initializing SQLite WASM...");
const startTime = performance.now();
this.sqlite3 = await sqlite3InitModule({
print: console.log,
printErr: console.error,
});
const initTime = performance.now() - startTime;
console.log(
`[BrowserSqlProvider] SQLite WASM initialized in ${initTime.toFixed(2)}ms:`,
this.sqlite3.version.libVersion
);
} catch (e) {
this.initError = e instanceof Error ? e : new Error(String(e));
console.error("[BrowserSqlProvider] SQLite WASM initialization failed:", this.initError);
throw this.initError;
}
}
/**
* Check if the SQLite WASM module has been initialized.
*/
get isInitialized(): boolean {
return this.sqlite3 !== undefined;
}
// ==================== OPFS Support ====================
/**
* Check if the OPFS VFS is available.
* This requires:
* - Running in a Worker context
* - Browser support for OPFS APIs
* - COOP/COEP headers sent by the server (for SharedArrayBuffer)
*
* @returns true if OPFS VFS is available for use
*/
isOpfsAvailable(): boolean {
this.ensureSqlite3();
// SQLite WASM automatically installs the OPFS VFS if the environment supports it
// We can check for its presence via sqlite3_vfs_find or the OpfsDb class
return this.sqlite3!.oo1.OpfsDb !== undefined;
}
/**
* Load or create a database stored in OPFS for persistent storage.
* The database will persist across browser sessions.
*
* Requires COOP/COEP headers to be set by the server:
* - Cross-Origin-Opener-Policy: same-origin
* - Cross-Origin-Embedder-Policy: require-corp
*
* @param path - The path for the database file in OPFS (e.g., "/trilium.db")
* Paths without a leading slash are treated as relative to OPFS root.
* Leading directories are created automatically.
* @param options - Additional options
* @throws Error if OPFS VFS is not available
*
* @example
* ```typescript
* const provider = new BrowserSqlProvider();
* await provider.initWasm();
* if (provider.isOpfsAvailable()) {
* provider.loadFromOpfs("/my-database.db");
* } else {
* console.warn("OPFS not available, using in-memory database");
* provider.loadFromMemory();
* }
* ```
*/
loadFromOpfs(path: string, options: { createIfNotExists?: boolean } = {}): void {
this.ensureSqlite3();
if (!this.isOpfsAvailable()) {
throw new Error(
"OPFS VFS is not available. This requires:\n" +
"1. Running in a Worker context\n" +
"2. Browser support for OPFS (Chrome 102+, Firefox 111+, Safari 17+)\n" +
"3. COOP/COEP headers from the server:\n" +
" Cross-Origin-Opener-Policy: same-origin\n" +
" Cross-Origin-Embedder-Policy: require-corp"
);
}
console.log(`[BrowserSqlProvider] Loading database from OPFS: ${path}`);
const startTime = performance.now();
try {
// OpfsDb automatically creates directories in the path
// Mode 'c' = create if not exists
const mode = options.createIfNotExists !== false ? 'c' : '';
this.db = new this.sqlite3!.oo1.OpfsDb(path, mode);
this.opfsDbPath = path;
// Configure the database for OPFS
// Note: WAL mode requires exclusive locking in OPFS environment
this.db.exec("PRAGMA journal_mode = DELETE");
this.db.exec("PRAGMA synchronous = NORMAL");
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] OPFS database loaded in ${loadTime.toFixed(2)}ms`);
} catch (e) {
const error = e instanceof Error ? e : new Error(String(e));
console.error(`[BrowserSqlProvider] Failed to load OPFS database: ${error.message}`);
throw error;
}
}
/**
* Check if the currently open database is stored in OPFS.
*/
get isUsingOpfs(): boolean {
return this.opfsDbPath !== undefined;
}
/**
* Get the OPFS path of the currently open database.
* Returns undefined if not using OPFS.
*/
get currentOpfsPath(): string | undefined {
return this.opfsDbPath;
}
/**
* Check if the database has been initialized with a schema.
* This is a simple sanity check that looks for the existence of core tables.
*
* @returns true if the database appears to be initialized
*/
isDbInitialized(): boolean {
this.ensureDb();
// Check if the 'notes' table exists (a core table that must exist in an initialized DB)
const tableExists = this.db!.selectValue(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'notes'"
);
return tableExists !== undefined;
}
// ==================== End OPFS Support ====================
loadFromFile(_path: string, _isReadOnly: boolean): void {
// Browser environment doesn't have direct file system access.
// Use OPFS for persistent storage.
throw new Error(
"loadFromFile is not supported in browser environment. " +
"Use loadFromMemory() for temporary databases, loadFromBuffer() to load from data, " +
"or loadFromOpfs() for persistent storage."
);
}
/**
* Create an empty in-memory database.
* Data will be lost when the page is closed.
*
* For persistent storage, use loadFromOpfs() instead.
* To load demo data, call initializeDemoDatabase() after this.
*/
loadFromMemory(): void {
this.ensureSqlite3();
console.log("[BrowserSqlProvider] Creating in-memory database...");
const startTime = performance.now();
this.db = new this.sqlite3!.oo1.DB(":memory:", "c");
this.opfsDbPath = undefined; // Not using OPFS
this.db.exec("PRAGMA journal_mode = WAL");
const loadTime = performance.now() - startTime;
console.log(`[BrowserSqlProvider] In-memory database created in ${loadTime.toFixed(2)}ms`);
}
loadFromBuffer(buffer: Uint8Array): void {
this.ensureSqlite3();
// SQLite WASM can deserialize a database from a byte array
const p = this.sqlite3!.wasm.allocFromTypedArray(buffer);
try {
this.db = new this.sqlite3!.oo1.DB({ filename: ":memory:", flags: "c" });
this.opfsDbPath = undefined; // Not using OPFS
const rc = this.sqlite3!.capi.sqlite3_deserialize(
this.db.pointer!,
"main",
p,
buffer.byteLength,
buffer.byteLength,
this.sqlite3!.capi.SQLITE_DESERIALIZE_FREEONCLOSE |
this.sqlite3!.capi.SQLITE_DESERIALIZE_RESIZEABLE
);
if (rc !== 0) {
throw new Error(`Failed to deserialize database: ${rc}`);
}
} catch (e) {
this.sqlite3!.wasm.dealloc(p);
throw e;
}
}
backup(_destinationFile: string): void {
// In browser, we can serialize the database to a byte array
// For actual file backup, we'd need to use File System Access API or download
throw new Error(
"backup to file is not supported in browser environment. " +
"Use serialize() to get the database as a Uint8Array instead."
);
}
/**
* Serialize the database to a byte array.
* This can be used to save the database to IndexedDB, download it, etc.
*/
serialize(): Uint8Array {
this.ensureDb();
// Use the convenience wrapper which handles all the memory management
return this.sqlite3!.capi.sqlite3_js_db_export(this.db!);
}
prepare(query: string): Statement {
this.ensureDb();
// Check if we already have this statement cached
if (this.statementCache.has(query)) {
return this.statementCache.get(query)!;
}
// Create new statement and cache it
const stmt = this.db!.prepare(query);
const wasmStatement = new WasmStatement(stmt, this.db!, this.sqlite3!, query);
this.statementCache.set(query, wasmStatement);
return wasmStatement;
}
transaction<T>(func: (statement: Statement) => T): Transaction {
this.ensureDb();
const self = this;
let savepointCounter = 0;
// Helper function to execute within a transaction
const executeTransaction = (beginStatement: string, ...args: unknown[]): T => {
// If we're already in a transaction, use SAVEPOINTs for nesting
// This mimics better-sqlite3's behavior
if (self._inTransaction) {
const savepointName = `sp_${++savepointCounter}_${Date.now()}`;
self.db!.exec(`SAVEPOINT ${savepointName}`);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec(`RELEASE SAVEPOINT ${savepointName}`);
return result;
} catch (e) {
self.db!.exec(`ROLLBACK TO SAVEPOINT ${savepointName}`);
throw e;
}
}
// Not in a transaction, start a new one
self._inTransaction = true;
self.db!.exec(beginStatement);
try {
const result = func.apply(null, args as [Statement]);
self.db!.exec("COMMIT");
return result;
} catch (e) {
self.db!.exec("ROLLBACK");
throw e;
} finally {
self._inTransaction = false;
}
};
// Create the transaction function that acts like better-sqlite3's Transaction interface
// In better-sqlite3, the transaction function is callable and has .deferred(), .immediate(), etc.
const transactionWrapper = Object.assign(
// Default call executes with BEGIN (same as immediate)
(...args: unknown[]): T => executeTransaction("BEGIN", ...args),
{
// Deferred transaction - locks acquired on first data access
deferred: (...args: unknown[]): T => executeTransaction("BEGIN DEFERRED", ...args),
// Immediate transaction - acquires write lock immediately
immediate: (...args: unknown[]): T => executeTransaction("BEGIN IMMEDIATE", ...args),
// Exclusive transaction - exclusive lock
exclusive: (...args: unknown[]): T => executeTransaction("BEGIN EXCLUSIVE", ...args),
// Default is same as calling directly
default: (...args: unknown[]): T => executeTransaction("BEGIN", ...args)
}
);
return transactionWrapper as unknown as Transaction;
}
get inTransaction(): boolean {
return this._inTransaction;
}
exec(query: string): void {
this.ensureDb();
this.db!.exec(query);
}
close(): void {
// Clean up all cached statements first
for (const statement of this.statementCache.values()) {
try {
statement.finalize();
} catch (e) {
// Ignore errors during cleanup
console.warn("Error finalizing statement during cleanup:", e);
}
}
this.statementCache.clear();
if (this.db) {
this.db.close();
this.db = undefined;
}
// Reset OPFS state
this.opfsDbPath = undefined;
}
/**
* Get the number of rows changed by the last INSERT, UPDATE, or DELETE statement.
*/
changes(): number {
this.ensureDb();
return this.db!.changes();
}
/**
* Check if the database is currently open.
*/
isOpen(): boolean {
return this.db !== undefined && this.db.isOpen();
}
private ensureSqlite3(): void {
if (!this.sqlite3) {
throw new Error(
"SQLite WASM module not initialized. Call initialize() first with the sqlite3 module."
);
}
}
private ensureDb(): void {
this.ensureSqlite3();
if (!this.db) {
throw new Error("Database not opened. Call loadFromMemory(), loadFromBuffer(), or loadFromOpfs() first.");
}
}
}

View File

@@ -0,0 +1,16 @@
import { LOCALE_IDS } from "@triliumnext/commons";
import type i18next from "i18next";
import I18NextHttpBackend from "i18next-http-backend";
export default async function translationProvider(i18nextInstance: typeof i18next, locale: LOCALE_IDS) {
await i18nextInstance.use(I18NextHttpBackend).init({
lng: locale,
fallbackLng: "en",
ns: "server",
backend: {
loadPath: `${import.meta.resolve("../server-assets/translations")}/{{lng}}/{{ns}}.json`
},
returnEmptyString: false,
debug: true
});
}

View File

@@ -0,0 +1,110 @@
import LocalServerWorker from "./local-server-worker?worker";
let localWorker: Worker | null = null;
const pending = new Map();
function showFatalErrorDialog(message: string) {
alert(message);
}
export function startLocalServerWorker() {
if (localWorker) return localWorker;
localWorker = new LocalServerWorker();
localWorker.postMessage({ type: "INIT", queryString: location.search });
// Handle worker errors during initialization
localWorker.onerror = (event) => {
console.error("[LocalBridge] Worker error:", event);
// Reject all pending requests
for (const [, resolver] of pending) {
resolver.reject(new Error(`Worker error: ${event.message}`));
}
pending.clear();
};
localWorker.onmessage = (event) => {
const msg = event.data;
// Handle fatal platform crashes (shown as a dialog to the user)
if (msg?.type === "FATAL_ERROR") {
console.error("[LocalBridge] Fatal error:", msg.message);
showFatalErrorDialog(msg.message);
return;
}
// Handle worker error reports
if (msg?.type === "WORKER_ERROR") {
console.error("[LocalBridge] Worker reported error:", msg.error);
// Reject all pending requests with the error
for (const [, resolver] of pending) {
resolver.reject(new Error(msg.error?.message || "Unknown worker error"));
}
pending.clear();
return;
}
// Handle WebSocket-like messages from the worker (for frontend updates)
if (msg?.type === "WS_MESSAGE" && msg.message) {
// Dispatch a custom event that ws.ts listens to in standalone mode
window.dispatchEvent(new CustomEvent("trilium:ws-message", {
detail: msg.message
}));
return;
}
if (!msg || msg.type !== "LOCAL_RESPONSE") return;
const { id, response, error } = msg;
const resolver = pending.get(id);
if (!resolver) return;
pending.delete(id);
if (error) resolver.reject(new Error(error));
else resolver.resolve(response);
};
return localWorker;
}
export function attachServiceWorkerBridge() {
navigator.serviceWorker.addEventListener("message", async (event) => {
const msg = event.data;
if (!msg || msg.type !== "LOCAL_FETCH") return;
const port = event.ports && event.ports[0];
if (!port) return;
try {
startLocalServerWorker();
const id = msg.id;
const req = msg.request;
const response = await new Promise<{ body?: ArrayBuffer }>((resolve, reject) => {
pending.set(id, { resolve, reject });
// Transfer body to worker for efficiency (if present)
localWorker!.postMessage({
type: "LOCAL_REQUEST",
id,
request: req
}, req.body ? [req.body] : []);
});
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id,
response
}, response.body ? [response.body] : []);
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
port.postMessage({
type: "LOCAL_FETCH_RESPONSE",
id: msg.id,
response: {
status: 500,
headers: { "content-type": "text/plain; charset=utf-8" },
body: new TextEncoder().encode(errorMessage).buffer
}
});
}
});
}

View File

@@ -0,0 +1,273 @@
// =============================================================================
// ERROR HANDLERS FIRST - No static imports above this!
// ES modules hoist static imports, so they execute BEFORE any code runs.
// We use dynamic imports below to ensure error handlers are registered first.
// =============================================================================
self.onerror = (message, source, lineno, colno, error) => {
const errorMsg = `[Worker] Uncaught error: ${message}\n at ${source}:${lineno}:${colno}`;
console.error(errorMsg, error);
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(message),
source,
lineno,
colno,
stack: error?.stack || new Error().stack
}
});
} catch (e) {
console.error("[Worker] Failed to report error:", e);
}
return false;
};
self.onunhandledrejection = (event) => {
const reason = event.reason;
const errorMsg = `[Worker] Unhandled rejection: ${reason?.message || reason}`;
console.error(errorMsg, reason);
try {
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(reason?.message || reason),
stack: reason?.stack || new Error().stack
}
});
} catch (e) {
console.error("[Worker] Failed to report rejection:", e);
}
};
console.log("[Worker] Error handlers installed, loading modules...");
// =============================================================================
// TYPE-ONLY IMPORTS (erased at runtime, safe as static imports)
// =============================================================================
import type { BrowserRouter } from './lightweight/browser_router';
// =============================================================================
// MODULE STATE (populated by dynamic imports)
// =============================================================================
let BrowserSqlProvider: typeof import('./lightweight/sql_provider').default;
let WorkerMessagingProvider: typeof import('./lightweight/messaging_provider').default;
let BrowserExecutionContext: typeof import('./lightweight/cls_provider').default;
let BrowserCryptoProvider: typeof import('./lightweight/crypto_provider').default;
let FetchRequestProvider: typeof import('./lightweight/request_provider').default;
let StandalonePlatformProvider: typeof import('./lightweight/platform_provider').default;
let translationProvider: typeof import('./lightweight/translation_provider').default;
let createConfiguredRouter: typeof import('./lightweight/browser_routes').createConfiguredRouter;
// Instance state
let sqlProvider: InstanceType<typeof BrowserSqlProvider> | null = null;
let messagingProvider: InstanceType<typeof WorkerMessagingProvider> | null = null;
// Core module, router, and initialization state
let coreModule: typeof import("@triliumnext/core") | null = null;
let router: BrowserRouter | null = null;
let initPromise: Promise<void> | null = null;
let initError: Error | null = null;
let queryString = "";
/**
* Load all required modules using dynamic imports.
* This allows errors to be caught by our error handlers.
*/
async function loadModules(): Promise<void> {
console.log("[Worker] Loading lightweight modules...");
const [
sqlModule,
messagingModule,
clsModule,
cryptoModule,
requestModule,
platformModule,
translationModule,
routesModule
] = await Promise.all([
import('./lightweight/sql_provider.js'),
import('./lightweight/messaging_provider.js'),
import('./lightweight/cls_provider.js'),
import('./lightweight/crypto_provider.js'),
import('./lightweight/request_provider.js'),
import('./lightweight/platform_provider.js'),
import('./lightweight/translation_provider.js'),
import('./lightweight/browser_routes.js')
]);
BrowserSqlProvider = sqlModule.default;
WorkerMessagingProvider = messagingModule.default;
BrowserExecutionContext = clsModule.default;
BrowserCryptoProvider = cryptoModule.default;
FetchRequestProvider = requestModule.default;
StandalonePlatformProvider = platformModule.default;
translationProvider = translationModule.default;
createConfiguredRouter = routesModule.createConfiguredRouter;
// Create instances
sqlProvider = new BrowserSqlProvider();
messagingProvider = new WorkerMessagingProvider();
console.log("[Worker] Lightweight modules loaded successfully");
}
/**
* Initialize SQLite WASM and load the core module.
* This happens once at worker startup.
*/
async function initialize(): Promise<void> {
if (initPromise) {
return initPromise; // Already initializing
}
if (initError) {
throw initError; // Failed before, don't retry
}
initPromise = (async () => {
try {
// First, load all modules dynamically
await loadModules();
console.log("[Worker] Initializing SQLite WASM...");
await sqlProvider!.initWasm();
// Try to use OPFS for persistent storage
if (sqlProvider!.isOpfsAvailable()) {
console.log("[Worker] OPFS available, loading persistent database...");
sqlProvider!.loadFromOpfs("/trilium.db");
} else {
// Fall back to in-memory database (non-persistent)
console.warn("[Worker] OPFS not available, using in-memory database (data will not persist)");
console.warn("[Worker] To enable persistence, ensure COOP/COEP headers are set by the server");
sqlProvider!.loadFromMemory();
}
console.log("[Worker] Database loaded");
console.log("[Worker] Loading @triliumnext/core...");
const schemaModule = await import("@triliumnext/core/src/assets/schema.sql?raw");
coreModule = await import("@triliumnext/core");
await coreModule.initializeCore({
executionContext: new BrowserExecutionContext(),
crypto: new BrowserCryptoProvider(),
messaging: messagingProvider!,
request: new FetchRequestProvider(),
platform: new StandalonePlatformProvider(queryString),
translations: translationProvider,
schema: schemaModule.default,
dbConfig: {
provider: sqlProvider!,
isReadOnly: false,
onTransactionCommit: () => {
coreModule?.ws.sendTransactionEntityChangesToAllClients();
},
onTransactionRollback: () => {
// No-op for now
}
}
});
coreModule.ws.init();
console.log("[Worker] Supported routes", Object.keys(coreModule.routes));
// Create and configure the router
router = createConfiguredRouter();
console.log("[Worker] Router configured");
// initializeDb runs initDbConnection inside an execution context,
// which resolves dbReady — required before beccaLoaded can settle.
coreModule.sql_init.initializeDb();
if (coreModule.sql_init.isDbInitialized()) {
console.log("[Worker] Database already initialized, loading becca...");
await coreModule.becca_loader.beccaLoaded;
} else {
console.log("[Worker] Database not initialized, skipping becca load (will be loaded during DB initialization)");
}
console.log("[Worker] Initialization complete");
} catch (error) {
initError = error instanceof Error ? error : new Error(String(error));
console.error("[Worker] Initialization failed:", initError);
throw initError;
}
})();
return initPromise;
}
/**
* Ensure the worker is initialized before processing requests.
* Returns the router if initialization was successful.
*/
async function ensureInitialized() {
await initialize();
if (!router) {
throw new Error("Router not initialized");
}
return router;
}
interface LocalRequest {
method: string;
url: string;
body?: unknown;
headers?: Record<string, string>;
}
// Main dispatch
async function dispatch(request: LocalRequest) {
// Ensure initialization is complete and get the router
const appRouter = await ensureInitialized();
// Dispatch to the router
return appRouter.dispatch(request.method, request.url, request.body, request.headers);
}
// Start initialization immediately when the worker loads
console.log("[Worker] Starting initialization...");
initialize().catch(err => {
console.error("[Worker] Initialization failed:", err);
// Post error to main thread
self.postMessage({
type: "WORKER_ERROR",
error: {
message: String(err?.message || err),
stack: err?.stack
}
});
});
self.onmessage = async (event) => {
const msg = event.data;
if (!msg) return;
if (msg.type === "INIT") {
queryString = msg.queryString || "";
return;
}
if (msg.type !== "LOCAL_REQUEST") return;
const { id, request } = msg;
try {
const response = await dispatch(request);
// Transfer body back (if any) - use options object for proper typing
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
response
}, { transfer: response.body ? [response.body] : [] });
} catch (e) {
console.error("[Worker] Dispatch error:", e);
(self as unknown as Worker).postMessage({
type: "LOCAL_RESPONSE",
id,
error: String((e as Error)?.message || e)
});
}
};

View File

@@ -0,0 +1,84 @@
import { attachServiceWorkerBridge, startLocalServerWorker } from "./local-bridge.js";
async function waitForServiceWorkerControl(): Promise<void> {
if (!("serviceWorker" in navigator)) {
throw new Error("Service Worker not supported in this browser");
}
// If already controlling, we're good
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker already controlling");
return;
}
console.log("[Bootstrap] Waiting for service worker to take control...");
// Register service worker
await navigator.serviceWorker.register("./sw.js", { scope: "/" });
// Wait for it to be ready (installed + activated)
await navigator.serviceWorker.ready;
// Check if we're now controlling
if (navigator.serviceWorker.controller) {
console.log("[Bootstrap] Service worker now controlling");
return;
}
// If not controlling yet, we need to reload the page for SW to take control
// This is standard PWA behavior on first install
console.log("[Bootstrap] Service worker installed but not controlling yet - reloading page");
// Wait a tiny bit for SW to fully activate
await new Promise(resolve => setTimeout(resolve, 100));
// Reload to let SW take control
window.location.reload();
// Throw to stop execution (page will reload)
throw new Error("Reloading for service worker activation");
}
async function bootstrap() {
/* fixes https://github.com/webpack/webpack/issues/10035 */
window.global = globalThis;
try {
// 1) Start local worker ASAP (so /bootstrap is fast)
startLocalServerWorker();
// 2) Bridge SW -> local worker
attachServiceWorkerBridge();
// 3) Wait for service worker to control the page (may reload on first install)
await waitForServiceWorkerControl();
await loadScripts();
} catch (err) {
// If error is from reload, it will stop here (page reloads)
// Otherwise, show error to user
if (err instanceof Error && err.message.includes("Reloading")) {
// Page is reloading, do nothing
return;
}
console.error("[Bootstrap] Fatal error:", err);
document.body.innerHTML = `
<div style="padding: 40px; max-width: 600px; margin: 0 auto; font-family: system-ui, sans-serif;">
<h1 style="color: #d32f2f;">Failed to Initialize</h1>
<p>The application failed to start. Please check the browser console for details.</p>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow: auto;">${err instanceof Error ? err.message : String(err)}</pre>
<button onclick="location.reload()" style="padding: 12px 24px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px;">
Reload Page
</button>
</div>
`;
document.body.style.display = "block";
}
}
async function loadScripts() {
await import("../../client/src/index.js");
}
bootstrap();

View File

@@ -0,0 +1,185 @@
// public/sw.js
const VERSION = "localserver-v1.4";
const STATIC_CACHE = `static-${VERSION}`;
// Check if running in dev mode (passed via URL parameter)
const isDev = true;
if (isDev) {
console.log('[Service Worker] Running in DEV mode - caching disabled');
}
// Adjust these to your routes:
const LOCAL_FIRST_PREFIXES = [
"/bootstrap",
"/api/",
"/sync/",
"/search/"
];
// Optional: basic precache list (keep small; you can expand later)
const PRECACHE_URLS = [
// "/",
// "/index.html",
// "/manifest.webmanifest",
// "/favicon.ico",
];
self.addEventListener("install", (event) => {
event.waitUntil((async () => {
// Skip precaching in dev mode
if (!isDev) {
const cache = await caches.open(STATIC_CACHE);
await cache.addAll(PRECACHE_URLS);
}
self.skipWaiting();
})());
});
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
// Cleanup old caches
const keys = await caches.keys();
await Promise.all(keys.map((k) => (k === STATIC_CACHE ? Promise.resolve() : caches.delete(k))));
await self.clients.claim();
})());
});
function isLocalFirst(url) {
return LOCAL_FIRST_PREFIXES.some((p) => url.pathname.startsWith(p));
}
async function cacheFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
}
async function networkFirst(request) {
// In dev mode, always bypass cache
if (isDev) {
return fetch(request);
}
const cache = await caches.open(STATIC_CACHE);
try {
const fresh = await fetch(request);
// Cache only successful GETs
if (request.method === "GET" && fresh.ok) cache.put(request, fresh.clone());
return fresh;
} catch (error) {
// Fallback to cache if network fails
const cached = await cache.match(request);
if (cached) return cached;
throw error;
}
}
async function forwardToClientLocalServer(request, clientId) {
// Find a client to handle the request (prefer the initiating client if available)
let client = clientId ? await self.clients.get(clientId) : null;
if (!client) {
const all = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
client = all[0] || null;
}
// If no page is available, fall back to network
if (!client) return fetch(request);
const reqUrl = request.url;
const headersObj = {};
for (const [k, v] of request.headers.entries()) headersObj[k] = v;
const body = (request.method === "GET" || request.method === "HEAD")
? null
: await request.arrayBuffer();
const id = crypto.randomUUID();
const channel = new MessageChannel();
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Local server timeout"));
}, 30_000);
channel.port1.onmessage = (event) => {
clearTimeout(timeout);
resolve(event.data);
};
channel.port1.onmessageerror = () => {
clearTimeout(timeout);
reject(new Error("Local server message error"));
};
});
// Send to the client with a reply port
client.postMessage({
type: "LOCAL_FETCH",
id,
request: {
url: reqUrl,
method: request.method,
headers: headersObj,
body // ArrayBuffer or null
}
}, [channel.port2]);
const localResp = await responsePromise;
if (!localResp || localResp.type !== "LOCAL_FETCH_RESPONSE" || localResp.id !== id) {
// Protocol mismatch; fall back
return fetch(request);
}
// localResp.response: { status, headers, body }
const { status, headers, body: respBody } = localResp.response;
const respHeaders = new Headers();
if (headers) {
for (const [k, v] of Object.entries(headers)) respHeaders.set(k, String(v));
}
return new Response(respBody ? respBody : null, {
status: status || 200,
headers: respHeaders
});
}
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
// Only handle same-origin
if (url.origin !== self.location.origin) return;
// HTML files: network-first to ensure updates are reflected immediately
if (event.request.mode === "navigate" || url.pathname.endsWith(".html")) {
event.respondWith(networkFirst(event.request));
return;
}
// Static assets: cache-first for performance
if (event.request.method === "GET" && !isLocalFirst(url)) {
event.respondWith(cacheFirst(event.request));
return;
}
// API-ish: local-first via bridge
if (isLocalFirst(url)) {
event.respondWith(forwardToClientLocalServer(event.request, event.clientId));
return;
}
// Default
event.respondWith(fetch(event.request));
});

View File

@@ -0,0 +1,31 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
interface Window {
glob: {
assetPath: string;
themeCssUrl?: string;
themeUseNextAsBase?: string;
iconPackCss: string;
device: string;
headingStyle: string;
layoutOrientation: string;
platform: string;
isElectron: boolean;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
currentLocale: {
id: string;
rtl: boolean;
};
activeDialog: any;
};
global: typeof globalThis;
}

View File

@@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [
"ES2022",
"dom",
"dom.iterable"
],
"skipLibCheck": true,
"types": [
"vite/client"
],
"jsx": "react-jsx",
"jsxImportSource": "preact"
},
"include": [
"src/**/*",
"../client/src/**/*"
],
"exclude": [
"src/**/*.spec.ts",
"src/**/*.test.ts",
"../client/src/**/*.spec.ts",
"../client/src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.spec.json" }
]
}

View File

@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [
"ES2022",
"dom",
"dom.iterable"
],
"types": [
"vitest/globals",
"happy-dom"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.test.ts"
]
}

View File

@@ -0,0 +1,188 @@
import prefresh from '@prefresh/vite';
import { join } from 'path';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
const clientAssets = ["assets", "stylesheets", "fonts", "translations"];
const isDev = process.env.NODE_ENV === "development";
// Watch client files and trigger reload in development
const clientWatchPlugin = () => ({
name: 'client-watch',
configureServer(server: any) {
if (isDev) {
// Watch client source files (adjusted for new root)
server.watcher.add('../../client/src/**/*');
server.watcher.on('change', (file: string) => {
if (file.includes('../../client/src/')) {
server.ws.send({
type: 'full-reload'
});
}
});
}
}
});
// Always copy SQLite WASM files so they're available to the module
const sqliteWasmPlugin = viteStaticCopy({
targets: [
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm",
dest: "assets"
},
{
src: "../../../node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js",
dest: "assets"
}
]
});
let plugins: any = [
sqliteWasmPlugin, // Always include SQLite WASM files
viteStaticCopy({
targets: clientAssets.map((asset) => ({
src: `../../client/src/${asset}/*`,
dest: asset
})),
// Enable watching in development
...(isDev && {
watch: {
reloadPageOnChange: true
}
})
}),
viteStaticCopy({
targets: [
{
src: "../../server/src/assets/*",
dest: "server-assets"
}
]
}),
// Watch client files for changes in development
...(isDev ? [
prefresh(),
clientWatchPlugin()
] : [])
];
if (!isDev) {
plugins = [
...plugins,
viteStaticCopy({
structured: true,
targets: [
{
src: "../../../node_modules/@excalidraw/excalidraw/dist/prod/fonts/*",
dest: "",
}
]
})
]
}
export default defineConfig(() => ({
root: join(__dirname, 'src'), // Set src as root so index.html is served from /
envDir: __dirname, // Load .env files from client-standalone directory, not src/
cacheDir: '../../../node_modules/.vite/apps/client-standalone',
base: "",
plugins,
esbuild: {
jsx: 'automatic',
jsxImportSource: 'preact',
jsxDev: isDev
},
css: {
transformer: 'lightningcss',
devSourcemap: isDev
},
publicDir: join(__dirname, 'public'),
resolve: {
alias: [
{
find: "react",
replacement: "preact/compat"
},
{
find: "react-dom",
replacement: "preact/compat"
},
{
find: "@client",
replacement: join(__dirname, "../client/src")
}
],
dedupe: [
"react",
"react-dom",
"preact",
"preact/compat",
"preact/hooks"
]
},
server: {
watch: {
// Watch workspace packages
ignored: ['!**/node_modules/@triliumnext/**'],
// Also watch client assets for live reload
usePolling: false,
interval: 100,
binaryInterval: 300
},
// Watch additional directories for changes
fs: {
allow: [
// Allow access to workspace root
'../../../',
// Explicitly allow client directory
'../../client/src/'
]
},
headers: {
// Required for SharedArrayBuffer which is needed by SQLite WASM OPFS VFS
// See: https://sqlite.org/wasm/doc/trunk/persistence.md#coop-coep
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
},
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm', '@triliumnext/core']
},
worker: {
format: "es" as const
},
commonjsOptions: {
transformMixedEsModules: true,
},
build: {
target: "esnext",
outDir: join(__dirname, 'dist'),
emptyOutDir: true,
rollupOptions: {
input: {
main: join(__dirname, 'src', 'index.html'),
sw: join(__dirname, 'src', 'sw.ts'),
'local-bridge': join(__dirname, 'src', 'local-bridge.ts'),
},
output: {
entryFileNames: (chunkInfo) => {
// Service worker and other workers should be at root level
if (chunkInfo.name === 'sw') {
return '[name].js';
}
return 'src/[name].js';
},
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]"
}
}
},
test: {
environment: "happy-dom"
},
define: {
"process.env.IS_PREACT": JSON.stringify("true"),
}
}));

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.2",
"version": "0.102.1",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -27,46 +27,48 @@
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@preact/signals": "2.9.0",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.8.2",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"@triliumnext/share-theme": "workspace:*",
"@triliumnext/split.js": "workspace:*",
"@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",
"@univerjs/preset-sheets-conditional-formatting": "0.18.0",
"@univerjs/preset-sheets-core": "0.18.0",
"@univerjs/preset-sheets-data-validation": "0.18.0",
"@univerjs/preset-sheets-filter": "0.18.0",
"@univerjs/preset-sheets-find-replace": "0.18.0",
"@univerjs/preset-sheets-note": "0.18.0",
"@univerjs/preset-sheets-sort": "0.18.0",
"@univerjs/presets": "0.18.0",
"@zumer/snapdom": "2.6.0",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.8",
"boxicons": "2.1.4",
"clsx": "2.1.1",
"color": "5.0.3",
"debounce": "3.0.0",
"dompurify": "3.3.3",
"draggabilly": "3.0.0",
"force-graph": "1.51.2",
"i18next": "26.0.3",
"i18next-http-backend": "3.0.4",
"globals": "17.4.0",
"i18next": "25.10.10",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.45",
"katex": "0.16.43",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.5",
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"mermaid": "11.13.0",
"mind-elixir": "5.9.3",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"preact": "10.29.1",
"react-i18next": "17.0.2",
"preact": "10.29.0",
"react-i18next": "16.6.6",
"react-window": "2.2.7",
"reveal.js": "6.0.0",
"rrule": "2.8.1",
@@ -84,9 +86,9 @@
"@types/mark.js": "8.11.12",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "14.0.0",
"happy-dom": "20.8.9",
"happy-dom": "20.8.8",
"lightningcss": "1.32.0",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "4.0.1"
"vite-plugin-static-copy": "3.4.0"
}
}
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 256 256" version="1.1" viewBox="0 0 256 256" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<title>Trilium Notes</title>
<style type="text/css">
.st0{fill:#95C980;}
.st1{fill:#72B755;}
.st2{fill:#4FA52B;}
.st3{fill:#EE8C89;}
.st4{fill:#E96562;}
.st5{fill:#E33F3B;}
.st6{fill:#EFB075;}
.st7{fill:#E99547;}
.st8{fill:#E47B19;}
</style>
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,11 +1,10 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
import { 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";
@@ -303,7 +302,6 @@ export type CommandMappings = {
ninthTab: CommandData;
lastTab: CommandData;
showNoteSource: CommandData;
showNoteOCRText: CommandData;
showSQLConsole: CommandData;
showBackendLog: CommandData;
showCheatsheet: CommandData;
@@ -510,7 +508,7 @@ type EventMappings = {
contentSafeMarginChanged: {
top: number;
noteContext: NoteContext;
};
}
};
export type EventListener<T extends EventNames> = {
@@ -564,7 +562,7 @@ export class AppContext extends Component {
*/
async earlyInit() {
await options.initializedPromise;
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
await initLocale();
}
setLayout(layout: Layout) {
@@ -579,6 +577,7 @@ export class AppContext extends Component {
this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000);
}

View File

@@ -148,19 +148,6 @@ export default class RootCommandExecutor extends Component {
}
}
async showNoteOCRTextCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
await appContext.tabManager.openTabWithNoteWithHoisting(notePath, {
activate: true,
viewScope: {
viewMode: "ocr"
}
});
}
}
async showAttachmentsCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();

View File

@@ -54,7 +54,7 @@ function initOnElectron() {
const currentWindow = electronRemote.getCurrentWindow();
const style = window.getComputedStyle(document.body);
initDarkOrLightMode();
initDarkOrLightMode(style);
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() {
function initDarkOrLightMode(style: CSSStyleDeclaration) {
let themeSource: typeof nativeTheme.themeSource = "system";
const themeStyle = window.glob.getThemeStyle();
if (themeStyle !== "auto") {
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
themeSource = themeStyle;
}

View File

@@ -1,6 +1,5 @@
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";
@@ -19,7 +18,7 @@ const RELATION = "relation";
* end user. Those types should be used only for checking against, they are
* not for direct use.
*/
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet" | "llmChat";
export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "spreadsheet";
export interface NotePathRecord {
isArchived: boolean;
@@ -1015,6 +1014,7 @@ 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,5 +1,3 @@
import { getThemeStyle } from "./services/theme";
async function bootstrap() {
showSplash();
await setupGlob();
@@ -38,9 +36,35 @@ async function setupGlob() {
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
window.glob = {
...json,
activeDialog: null
activeDialog: null,
device: json.device || getDevice()
};
window.glob.getThemeStyle = getThemeStyle;
}
function getDevice() {
// Respect user's manual override via URL.
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("print")) {
return "print";
} else if (urlParams.has("desktop")) {
return "desktop";
} else if (urlParams.has("mobile")) {
return "mobile";
}
const deviceCookie = document.cookie.split("; ").find(row => row.startsWith("trilium-device="))?.split("=")[1];
if (deviceCookie === "desktop" || deviceCookie === "mobile") return deviceCookie;
return isMobile() ? "mobile" : "desktop";
}
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
if ("orientation" in window) return true;
const userAgentsRegEx = /\b(Android|iPhone|iPad|iPod|Windows Phone|BlackBerry|webOS|IEMobile)\b/i;
return userAgentsRegEx.test(navigator.userAgent);
}
async function loadBootstrapCss() {
@@ -52,65 +76,31 @@ async function loadBootstrapCss() {
}
}
type StylesheetRef = {
href: string;
media?: string;
};
function getConfiguredThemeStylesheets(stylesheetsPath: string, theme: string, customThemeCssUrl?: string) {
if (theme === "auto") {
return [{ href: `${stylesheetsPath}/theme-dark.css`, media: "(prefers-color-scheme: dark)" }];
}
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 { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
const cssToLoad: StylesheetRef[] = [];
const cssToLoad: string[] = [];
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(`${assetPath}/stylesheets/ckeditor-theme.css`);
cssToLoad.push(`api/fonts`);
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
if (themeCssUrl) {
cssToLoad.push(themeCssUrl);
}
cssToLoad.push({ href: `${stylesheetsPath}/style.css` });
if (themeUseNextAsBase === "next") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
} else if (themeUseNextAsBase === "next-dark") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
} else if (themeUseNextAsBase === "next-light") {
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
}
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
}
for (const { href, media } of cssToLoad) {
for (const href of cssToLoad) {
const linkEl = document.createElement("link");
linkEl.href = href;
linkEl.rel = "stylesheet";
if (media) {
linkEl.media = media;
}
document.head.appendChild(linkEl);
}
}
@@ -122,6 +112,8 @@ function loadIcons() {
}
function setBodyAttributes() {
if (!glob.dbInitialized) return;
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale } = window.glob;
const classesToSet = [
device,
@@ -142,6 +134,11 @@ function setBodyAttributes() {
}
async function loadScripts() {
if (!glob.dbInitialized) {
await import("./setup.js");
return;
}
switch (glob.device) {
case "mobile":
await import("./mobile.js");

View File

@@ -30,6 +30,7 @@ import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar.jsx";
import StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
@@ -186,6 +187,7 @@ export default class DesktopLayout {
)
)
.optChild(launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(<CloseZenModeButton />)
// Desktop-specific dialogs.

View File

@@ -13,6 +13,7 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StandaloneWarningBar from "../widgets/layout/StandaloneWarningBar";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
@@ -55,6 +56,7 @@ export default class MobileLayout {
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.optChild(glob.isStandalone, <StandaloneWarningBar />)
.child(
new FlexContainer("row")
.class("title-row note-split-title")

View File

@@ -1,14 +1,12 @@
import type { BrowserWindow } from "electron";
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 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";
function setupContextMenu() {
const electron = utils.dynamicRequire("electron");
@@ -17,6 +15,8 @@ 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: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
});
items.push({ kind: "separator" });
@@ -141,7 +141,7 @@ function setupContextMenu() {
}
// Replace the placeholder with the real search keyword.
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" });
@@ -155,6 +155,10 @@ 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,7 +4,6 @@ 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";
@@ -31,6 +30,7 @@ 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: Entity | null = null, script: string | null = null, params: string | null = null) {
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
script,
params

View File

@@ -1,6 +1,3 @@
import { t } from "./i18n.js";
import toast from "./toast.js";
export function copyText(text: string) {
if (!text) {
return;
@@ -9,26 +6,29 @@ export function copyText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
}
// 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);
} 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);
}
}
} catch (e) {
console.warn(e);
return false;
}
}
export function copyTextWithToast(text: string) {
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {

View File

@@ -1,6 +1,6 @@
import "./content_renderer.css";
import { normalizeMimeTypeForCKEditor, type TextRepresentationResponse } from "@triliumnext/commons";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import { h, render } from "preact";
import WheelZoom from 'vanilla-js-wheel-zoom';
@@ -15,7 +15,6 @@ import openService from "./open.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import renderService from "./render.js";
import server from "./server.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import utils, { getErrorMessage } from "./utils.js";
@@ -33,7 +32,6 @@ export interface RenderOptions {
includeArchivedNotes?: boolean;
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
seenNoteIds?: Set<string>;
showTextRepresentation?: boolean;
}
const CODE_MIME_TYPES = new Set(["application/json"]);
@@ -57,9 +55,9 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
} else if (type === "code") {
await renderCode(entity, $renderedContent);
} else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) {
await renderImage(entity, $renderedContent, options);
renderImage(entity, $renderedContent, options);
} else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) {
await renderFile(entity, type, $renderedContent, options);
await renderFile(entity, type, $renderedContent);
} else if (type === "mermaid") {
await renderMermaid(entity, $renderedContent);
} else if (type === "render" && entity instanceof FNote) {
@@ -140,7 +138,7 @@ async function renderCode(note: FNote | FAttachment, $renderedContent: JQuery<HT
await applySingleBlockSyntaxHighlight($codeBlock, normalizeMimeTypeForCKEditor(note.mime));
}
async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
const encodedTitle = encodeURIComponent(entity.title);
let url;
@@ -148,14 +146,13 @@ async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery
if (entity instanceof FNote) {
url = `api/images/${entity.noteId}/${encodedTitle}?${Math.random()}`;
} else if (entity instanceof FAttachment) {
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}`;
url = `api/attachments/${entity.attachmentId}/image/${encodedTitle}?${entity.utcDateModified}">`;
}
$renderedContent // styles needed for the zoom to work well
.css("display", "flex")
.css("align-items", "center")
.css("justify-content", "center")
.css("flex-direction", "column"); // OCR text is displayed below the image.
.css("justify-content", "center");
const $img = $("<img>")
.attr("src", url || "")
@@ -181,35 +178,9 @@ async function renderImage(entity: FNote | FAttachment, $renderedContent: JQuery
}
imageContextMenuService.setupContextMenu($img);
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $renderedContent);
}
}
async function addOCRTextIfAvailable(note: FNote, $content: JQuery<HTMLElement>) {
try {
const data = await server.get<TextRepresentationResponse>(`ocr/notes/${note.noteId}/text`);
if (data.success && data.hasOcr && data.text) {
const $ocrSection = $(`
<div class="ocr-text-section">
<div class="ocr-header">
<span class="bx bx-text"></span> ${t("ocr.extracted_text")}
</div>
<div class="ocr-content"></div>
</div>
`);
$ocrSection.find('.ocr-content').text(data.text);
$content.append($ocrSection);
}
} catch (error) {
// Silently fail if OCR API is not available
console.debug('Failed to fetch OCR text:', error);
}
}
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>, options: RenderOptions = {}) {
async function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: JQuery<HTMLElement>) {
let entityType, entityId;
if (entity instanceof FNote) {
@@ -249,10 +220,6 @@ async function renderFile(entity: FNote | FAttachment, type: string, $renderedCo
$content.append($videoPreview);
}
if (entity instanceof FNote && options.showTextRepresentation) {
await addOCRTextIfAvailable(entity, $content);
}
if (entityType === "notes" && "noteId" in entity) {
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
// in attachment list

View File

@@ -84,55 +84,6 @@ async function createSearchNote(opts = {}) {
return await froca.getNote(note.noteId);
}
async function createLlmChat() {
const note = await server.post<FNoteRow>("special-notes/llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recently modified LLM chat.
* Returns null if no chat exists.
*/
async function getMostRecentLlmChat() {
const note = await server.get<FNoteRow | null>("special-notes/most-recent-llm-chat");
if (!note) {
return null;
}
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
/**
* Gets the most recent LLM chat, or creates a new one if none exists.
* Used by sidebar chat for persistent conversations across page refreshes.
*/
async function getOrCreateLlmChat() {
const note = await server.get<FNoteRow>("special-notes/get-or-create-llm-chat");
await ws.waitForMaxKnownEntityChangeId();
return await froca.getNote(note.noteId);
}
export interface RecentLlmChat {
noteId: string;
title: string;
dateModified: string;
}
/**
* Gets a list of recent LLM chats for the history popup.
*/
async function getRecentLlmChats(limit: number = 10): Promise<RecentLlmChat[]> {
return await server.get<RecentLlmChat[]>(`special-notes/recent-llm-chats?limit=${limit}`);
}
export default {
getInboxNote,
getTodayNote,
@@ -143,9 +94,5 @@ export default {
getMonthNote,
getYearNote,
createSqlConsole,
createSearchNote,
createLlmChat,
getMostRecentLlmChat,
getOrCreateLlmChat,
getRecentLlmChats
createSearchNote
};

View File

@@ -1,11 +1,9 @@
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 keyboardActionsService from "./keyboard_actions.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -27,6 +25,7 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;

View File

@@ -1,30 +0,0 @@
import { describe, expect, it } from "vitest";
import { isValidDocName } from "./doc_renderer.js";
describe("isValidDocName", () => {
it("accepts valid docNames", () => {
expect(isValidDocName("launchbar_intro")).toBe(true);
expect(isValidDocName("User Guide/Quick Start")).toBe(true);
expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
expect(isValidDocName("Quick Start Guide")).toBe(true);
expect(isValidDocName("quick_start_guide")).toBe(true);
expect(isValidDocName("quick-start-guide")).toBe(true);
});
it("rejects path traversal attacks", () => {
expect(isValidDocName("..")).toBe(false);
expect(isValidDocName("../etc/passwd")).toBe(false);
expect(isValidDocName("foo/../bar")).toBe(false);
expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
expect(isValidDocName("..\\etc\\passwd")).toBe(false);
expect(isValidDocName("foo\\bar")).toBe(false);
});
it("rejects URL manipulation attacks", () => {
expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
expect(isValidDocName("foo#bar")).toBe(false);
expect(isValidDocName("%2e%2e")).toBe(false);
expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
});
});

View File

@@ -3,39 +3,22 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
/**
* Validates a docName to prevent path traversal attacks.
* Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
* but blocks traversal sequences and URL manipulation characters.
*/
export function isValidDocName(docName: string): boolean {
// Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes.
const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/;
return validDocNameRegex.test(docName);
}
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
const docName = note.getLabelValue("docName");
const $content = $("<div>");
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
if (url) {
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
$content.load(url, async (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
if (fallbackUrl) {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
});
} else {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
}
});
return;
}
@@ -45,6 +28,8 @@ export default function renderDoc(note: FNote) {
} else {
resolve($content);
}
return $content;
});
}
@@ -52,7 +37,7 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
const dir = url.substring(0, url.lastIndexOf("/"));
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
$content.find("img").each((i, el) => {
$content.find("img").each((_i, el) => {
const $img = $(el);
$img.attr("src", `${dir}/${$img.attr("src")}`);
});
@@ -63,17 +48,20 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string | null, language: string) {
if (!docNameValue) return;
if (!isValidDocName(docNameValue)) {
console.error(`Invalid docName: ${docNameValue}`);
return null;
}
function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
const basePath = window.glob.isDev ? `${window.glob.assetPath }/..` : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
// The user guide is available only in English, so make sure we are requesting correctly since 404s in standalone client are treated differently.
if (docNameValue.includes("User%20Guide")) language = "en";
return `${getBasePath()}/doc_notes/${language}/${docNameValue}.html`;
}
function getBasePath() {
if (window.glob.isStandalone) {
return `server-assets`;
}
if (window.glob.isDev) {
return `${window.glob.assetPath }/..`;
}
return window.glob.assetPath;
}

View File

@@ -13,11 +13,6 @@ export const experimentalFeatures = [
id: "new-layout",
name: t("experimental_features.new_layout_name"),
description: t("experimental_features.new_layout_description"),
},
{
id: "llm",
name: t("experimental_features.llm_name"),
description: t("experimental_features.llm_description"),
}
] as const satisfies ExperimentalFeature[];

View File

@@ -1,11 +1,11 @@
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FNote, { type FNoteRow } from "../entities/fnote.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import server from "./server.js";
import appContext from "../components/app_context.js";
import FBlob, { type FBlobRow } from "../entities/fblob.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import type { Froca } from "./froca-interface.js";
import server from "./server.js";
interface SubtreeResponse {
notes: FNoteRow[];
@@ -44,8 +44,9 @@ class FrocaImpl implements Froca {
}
async loadInitialTree() {
const resp = await server.get<SubtreeResponse>("tree");
if (!glob.dbInitialized) return;
const resp = await server.get<SubtreeResponse>("tree");
// clear the cache only directly before adding new content which is important for e.g., switching to protected session
this.#clear();
this.addResp(resp);
@@ -77,7 +78,7 @@ class FrocaImpl implements Froca {
for (const noteRow of noteRows) {
const { noteId } = noteRow;
let note = this.notes[noteId];
const note = this.notes[noteId];
if (note) {
note.update(noteRow);
@@ -240,9 +241,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -263,9 +263,8 @@ class FrocaImpl implements Froca {
console.trace(`Can't find note '${noteId}'`);
return null;
} else {
return this.notes[noteId];
}
return this.notes[noteId];
})
.filter((note) => !!note) as FNote[];
}
@@ -338,11 +337,10 @@ class FrocaImpl implements Froca {
attachmentRows = await server.getWithSilentNotFound<FAttachmentRow[]>(`attachments/${attachmentId}/all`);
} catch (e: any) {
if (silentNotFoundError) {
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ` + e.message);
logInfo(`Attachment '${attachmentId}' not found, but silentNotFoundError is enabled: ${e.message}`);
return null;
} else {
throw e;
}
throw e;
}
const attachments = this.processAttachmentRows(attachmentRows);

View File

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

View File

@@ -206,7 +206,7 @@ export interface Api {
* Instance name identifies particular Trilium instance. It can be useful for scripts
* if some action needs to happen on only one specific instance.
*/
getInstanceName(): string;
getInstanceName(): string | null;
/**
* @returns date in YYYY-MM-DD format

View File

@@ -1,4 +1,4 @@
import { createContext, Fragment, h, VNode } from "preact";
import { Fragment, h, VNode } from "preact";
import * as hooks from "preact/hooks";
import ActionButton from "../widgets/react/ActionButton";
@@ -47,7 +47,6 @@ 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,14 +1,22 @@
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
import { type Locale, LOCALE_IDS, setDayjsLocale } from "@triliumnext/commons";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import options from "./options.js";
import server from "./server.js";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export const translationsInitializedPromise = $.Deferred();
export async function initLocale(locale: LOCALE_IDS = "en") {
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales");
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
@@ -17,7 +25,8 @@ export async function initLocale(locale: LOCALE_IDS = "en") {
backend: {
loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json`
},
returnEmptyString: false
returnEmptyString: false,
showSupportNotice: false
});
await setDayjsLocale(locale);
@@ -25,7 +34,11 @@ export async function initLocale(locale: LOCALE_IDS = "en") {
}
export function getAvailableLocales() {
return LOCALES;
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.");
}
return locales;
}
/**
@@ -36,7 +49,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

@@ -19,8 +19,7 @@ export const byNoteType: Record<Exclude<NoteType, "book">, string | null> = {
search: null,
text: null,
webView: null,
spreadsheet: null,
llmChat: null
spreadsheet: null
};
export const byBookType: Record<ViewTypeOptions, string | null> = {

View File

@@ -28,7 +28,7 @@ async function getLinkIcon(noteId: string, viewMode: ViewMode | undefined) {
return icon;
}
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map" | "ocr";
export type ViewMode = "default" | "source" | "attachments" | "contextual-help" | "note-map";
export interface ViewScope {
/**

View File

@@ -1,114 +0,0 @@
import type { LlmChatConfig, LlmCitation, LlmMessage, LlmModelInfo,LlmUsage } from "@triliumnext/commons";
import server from "./server.js";
/**
* Fetch available models from all configured providers.
*/
export async function getAvailableModels(): Promise<LlmModelInfo[]> {
const response = await server.get<{ models?: LlmModelInfo[] }>("llm-chat/models");
return response.models ?? [];
}
export interface StreamCallbacks {
onChunk: (text: string) => void;
onThinking?: (text: string) => void;
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
onToolResult?: (toolName: string, result: string, isError?: boolean) => void;
onCitation?: (citation: LlmCitation) => void;
onUsage?: (usage: LlmUsage) => void;
onError: (error: string) => void;
onDone: () => void;
}
/**
* Stream a chat completion from the LLM API using Server-Sent Events.
*/
export async function streamChatCompletion(
messages: LlmMessage[],
config: LlmChatConfig,
callbacks: StreamCallbacks
): Promise<void> {
const headers = await server.getHeaders();
const response = await fetch(`${window.glob.baseApiUrl}llm-chat/stream`, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json"
} as HeadersInit,
body: JSON.stringify({ messages, config })
});
if (!response.ok) {
callbacks.onError(`HTTP ${response.status}: ${response.statusText}`);
return;
}
const reader = response.body?.getReader();
if (!reader) {
callbacks.onError("No response body");
return;
}
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
try {
const data = JSON.parse(line.slice(6));
switch (data.type) {
case "text":
callbacks.onChunk(data.content);
break;
case "thinking":
callbacks.onThinking?.(data.content);
break;
case "tool_use":
callbacks.onToolUse?.(data.toolName, data.toolInput);
// Yield to force Preact to commit the pending tool call
// state before we process the result.
await new Promise((r) => setTimeout(r, 1));
break;
case "tool_result":
callbacks.onToolResult?.(data.toolName, data.result, data.isError);
await new Promise((r) => setTimeout(r, 1));
break;
case "citation":
if (data.citation) {
callbacks.onCitation?.(data.citation);
}
break;
case "usage":
if (data.usage) {
callbacks.onUsage?.(data.usage);
}
break;
case "error":
callbacks.onError(data.error);
break;
case "done":
callbacks.onDone();
break;
}
} catch (e) {
console.error("Failed to parse SSE data line:", line, e);
}
}
}
}
} finally {
reader.releaseLock();
}
}

View File

@@ -1,7 +1,6 @@
import type { NoteType } from "../entities/fnote.js";
import type { MenuCommandItem, MenuItem, MenuItemBadge, MenuSeparatorItem } from "../menus/context_menu.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import { isExperimentalFeatureEnabled } from "./experimental_features.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import server from "./server.js";
@@ -42,7 +41,6 @@ export const NOTE_TYPES: NoteTypeMapping[] = [
{ type: "relationMap", mime: "application/json", title: t("note_types.relation-map"), icon: "bxs-network-chart" },
// Misc note types
{ type: "llmChat", mime: "application/json", title: t("note_types.llm-chat"), icon: "bx-message-square-dots", isBeta: true },
{ type: "render", mime: "", title: t("note_types.render-note"), icon: "bx-extension" },
{ type: "search", title: t("note_types.saved-search"), icon: "bx-file-find", static: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), icon: "bx-globe-alt" },
@@ -94,7 +92,6 @@ async function getNoteTypeItems(command?: TreeCommandNames) {
function getBlankNoteTypes(command?: TreeCommandNames): MenuItem<TreeCommandNames>[] {
return NOTE_TYPES
.filter((nt) => !nt.reserved && nt.type !== "book")
.filter((nt) => nt.type !== "llmChat" || isExperimentalFeatureEnabled("llm"))
.map((nt) => {
const menuItem: MenuCommandItem<TreeCommandNames> = {
title: nt.title,

View File

@@ -1,14 +1,4 @@
export type LabelType = "text" | "textarea" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
type Multiplicity = "single" | "multi";
export interface DefinitionObject {
isPromoted?: boolean;
labelType?: LabelType;
multiplicity?: Multiplicity;
numberPrecision?: number;
promotedAlias?: string;
inverseRelation?: string;
}
import { DefinitionObject, LabelType, Multiplicity } from "@triliumnext/commons";
function parse(value: string) {
const tokens = value.split(",").map((t) => t.trim());

View File

@@ -1,4 +1,3 @@
import { t } from "./i18n.js";
import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js";
@@ -33,7 +32,8 @@ async function getHeaders(headers?: Headers) {
return {};
}
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? 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
@@ -270,11 +270,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, opts
} else if (opts.silentInternalServerError && jqXhr.status === 500) {
// report nothing
} else {
try {
await reportError(method, url, jqXhr.status, jqXhr.responseText);
} catch {
// reportError may throw (e.g. ValidationError); ensure rej() is still called below.
}
await reportError(method, url, jqXhr.status, jqXhr.responseText);
}
rej(jqXhr.responseText);
@@ -344,7 +340,6 @@ 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)) || "-";
@@ -358,6 +353,7 @@ 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,7 +367,8 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
window.logError(`${statusCode} ${method} ${url} - ${message}`);
const { logError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -1,35 +0,0 @@
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

@@ -135,6 +135,8 @@ export function isElectron() {
return !!(window && window.process && window.process.type);
}
export const isStandalone = window.glob.isStandalone;
/**
* Returns `true` if the client is running as a PWA, otherwise `false`.
*/
@@ -455,7 +457,9 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
const activeContext = glob.appContext?.tabManager?.getActiveContext();
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
if (!activeContext) {
return;
}
@@ -465,7 +469,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];
glob.appContext?.triggerCommand("openNewNoteSplit", {
appContext.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,
@@ -814,7 +818,7 @@ function compareVersions(v1: string, v2: string): number {
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
*/
function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
export function isUpdateAvailable(latestVersion: string | null | undefined, currentVersion: string): boolean {
if (!latestVersion) {
return false;
}
@@ -901,6 +905,10 @@ export function getErrorMessage(e: unknown) {
}
export function replaceHtmlEscapedSlashes(str: string) {
return str.replace(/&#x2F;/g, "/");
}
/**
* Handles left or right placement of e.g. tooltips in case of right-to-left languages. If the current language is a RTL one, then left and right are swapped. Other directions are unaffected.
* @param placement a string optionally containing a "left" or "right" value.
@@ -920,7 +928,6 @@ export default {
parseDate,
formatDateISO,
formatDateTime,
formatTime,
formatTimeInterval,
formatSize,
localNowDateTime,

View File

@@ -2,22 +2,20 @@ 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 toastService from "./toast.js";
import utils from "./utils.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
let ws: WebSocket;
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastAcceptedEntityChangeSyncId = window.glob.maxEntityChangeSyncIdAtLoad;
let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad;
let lastAcceptedEntityChangeId = window.glob.maxEntityChangeIdAtLoad ?? 0;
let lastAcceptedEntityChangeSyncId = window.glob.maxEntityChangeSyncIdAtLoad ?? 0;
let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad ?? 0;
let lastPingTs: number;
let frontendUpdateDataQueue: EntityChange[] = [];
@@ -59,6 +57,49 @@ export function unsubscribeToMessage(messageHandler: MessageHandler) {
messageHandlers = messageHandlers.filter(handler => handler !== messageHandler);
}
/**
* Dispatch a message to all handlers and process it.
* This is the main entry point for incoming messages from any provider
* (WebSocket, Worker, etc.)
*/
export async function dispatchMessage(message: WebSocketMessage) {
// Notify all subscribers
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
// Use string type for flexibility - server sends more message types than are typed
const messageType = message.type as string;
const msg = message as any;
// Process the message
if (messageType === "ping") {
lastPingTs = Date.now();
} else if (messageType === "reload-frontend") {
utils.reloadFrontendApp("received request from backend to reload frontend");
} else if (messageType === "frontend-update") {
await executeFrontendUpdate(msg.data.entityChanges);
} else if (messageType === "sync-hash-check-failed") {
toastService.showError(t("ws.sync-check-failed"), 60000);
} else if (messageType === "consistency-checks-failed") {
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (messageType === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: msg.noteId, messages: msg.messages });
} else if (messageType === "toast") {
toastService.showMessage(msg.message);
} else if (messageType === "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 = msg.originEntityId ? await froca.getNote(msg.originEntityId) : null;
bundleService.getAndExecuteBundle(msg.currentNoteId, originEntity, msg.script, msg.params);
}
}
// used to serialize frontend update operations
let consumeQueuePromise: Promise<void> | null = null;
@@ -114,32 +155,13 @@ async function executeFrontendUpdate(entityChanges: EntityChange[]) {
}
}
async function handleMessage(event: MessageEvent<any>) {
const message = JSON.parse(event.data);
for (const messageHandler of messageHandlers) {
messageHandler(message);
}
if (message.type === "ping") {
lastPingTs = Date.now();
} else if (message.type === "reload-frontend") {
utils.reloadFrontendApp("received request from backend to reload frontend");
} else if (message.type === "frontend-update") {
await executeFrontendUpdate(message.data.entityChanges);
} else if (message.type === "sync-hash-check-failed") {
toast.showError(t("ws.sync-check-failed"), 60000);
} else if (message.type === "consistency-checks-failed") {
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") {
toast.showMessage(message.message);
} else if (message.type === "execute-script") {
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
}
/**
* WebSocket message handler - parses the event and dispatches to generic handler.
* This is only used in WebSocket mode (not standalone).
*/
async function handleWebSocketMessage(event: MessageEvent<string>) {
const message = JSON.parse(event.data) as WebSocketMessage;
await dispatchMessage(message);
}
let entityChangeIdReachedListeners: {
@@ -201,7 +223,7 @@ async function consumeFrontendUpdateData() {
} else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toast.showError(t("ws.encountered-error", { message: e.message }));
toastService.showError(t("ws.encountered-error", { message: e.message }));
}
}
@@ -224,16 +246,21 @@ function connectWebSocket() {
// use wss for secure messaging
const ws = new WebSocket(webSocketUri);
ws.onopen = () => console.debug(utils.now(), `Connected to server ${webSocketUri} with WebSocket`);
ws.onmessage = handleMessage;
ws.onmessage = handleWebSocketMessage;
// we're not handling ws.onclose here because reconnection is done in sendPing()
return ws;
}
async function sendPing() {
if (!ws) {
// In standalone mode, there's no WebSocket — nothing to ping.
return;
}
if (Date.now() - lastPingTs > 30000) {
console.warn(utils.now(), "Lost websocket connection to the backend");
toast.showPersistent({
toastService.showPersistent({
id: "lost-websocket-connection",
title: t("ws.lost-websocket-connection-title"),
message: t("ws.lost-websocket-connection-message"),
@@ -242,7 +269,7 @@ async function sendPing() {
}
if (ws.readyState === ws.OPEN) {
toast.closePersistent("lost-websocket-connection");
toastService.closePersistent("lost-websocket-connection");
ws.send(
JSON.stringify({
type: "ping",
@@ -258,7 +285,18 @@ async function sendPing() {
setTimeout(() => {
if (glob.device === "print") return;
if (!glob.dbInitialized) return;
if (glob.isStandalone) {
// In standalone mode, listen for messages from the local worker via custom event
window.addEventListener("trilium:ws-message", ((event: CustomEvent<WebSocketMessage>) => {
dispatchMessage(event.detail);
}) as EventListener);
console.debug(utils.now(), "Standalone mode: listening for worker messages");
return;
}
// Normal mode: use WebSocket
ws = connectWebSocket();
lastPingTs = Date.now();

420
apps/client/src/setup.css Normal file
View File

@@ -0,0 +1,420 @@
html,
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
}
body.setup {
margin: 0;
padding: 0;
&>.setup-outer-wrapper {
width: 100dvw;
height: 100dvh;
body:not(.electron) & {
@media (min-width: 700px) {
background:
radial-gradient(ellipse at 20% 50%, rgba(99, 102, 241, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(168, 85, 247, 0.25) 0%, transparent 50%),
radial-gradient(ellipse at 60% 80%, rgba(59, 130, 246, 0.25) 0%, transparent 50%),
var(--left-pane-background-color);
display: flex;
justify-content: center;
align-items: center;
padding: 2em;
}
}
.setup-container {
background-color: var(--main-background-color);
border-radius: 16px;
padding: 2em;
flex-direction: column;
gap: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
position: relative;
height: 100%;
@media (min-width: 700px) {
display: flex;
width: 750px;
height: 650px;
top: unset;
overflow: hidden;
}
.setup-options {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
.setup-option-card {
padding: 1.5em;
cursor: pointer;
display: flex;
align-items: center;
gap: 1rem;
&.disabled {
cursor: not-allowed;
opacity: 0.5;
background-color: transparent;
border-color: var(--main-border-color)
}
&:not(.disabled):hover {
background-color: var(--card-background-hover-color);
filter: contrast(105%);
transition: background-color .2s ease-out;
}
.tn-icon {
font-size: 2.5em;
color: var(--muted-text-color);
}
h3 {
font-size: 1.5em;
font-weight: normal;
}
p:last-of-type {
margin-bottom: 0;
color: var(--muted-text-color);
}
}
}
}
.page {
display: flex;
flex-direction: column;
height: 100%;
padding: 2em;
overflow: auto;
>.back-button {
position: absolute;
top: 2em;
left: 2em;
color: var(--muted-text-color);
.tn-icon {
margin-right: 0.4em;
}
}
>main {
flex: 1;
display: flex;
flex-direction: column;
padding-top: 1em;
min-height: 0;
}
&.contentless {
justify-content: center;
align-items: center;
}
>footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
border-top: 1px solid var(--main-border-color);
padding-top: 1rem;
margin-inline: -2em;
padding-inline: 2em;
}
>.page-error {
position: absolute;
top: 0;
left: 0;
right: 0;
background: var(--admonition-caution-accent-color);
z-index: 1;
margin: 0;
border-radius: 0;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
padding-right: 2.5em;
button {
position: absolute;
top: 0.5em;
right: 0.5em;
}
}
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
width: 80%;
margin-inline: auto;
.form-group {
margin-bottom: 0;
}
.admonition {
margin: 0;
}
}
.form-item-with-icon {
display: flex;
align-items: center;
gap: 0.5rem;
.tn-icon {
font-size: 1.5em;
color: var(--muted-text-color);
}
}
}
.sync-illustration {
display: flex;
justify-content: center;
margin-top: 1.5em;
margin-bottom: 1.5rem;
.tn-icon {
font-size: 3em;
color: var(--muted-text-color);
}
>div {
display: flex;
flex-direction: column;
text-align: center;
gap: 0.5rem;
line-height: 1;
font-size: 0.85rem;
}
.sync-illustration-arrows {
width: 60px;
height: 3em;
position: relative;
&::after {
content: "";
position: absolute;
border: 2px dashed var(--main-border-color);
top: 1.5em;
left: 0;
right: 0;
}
}
}
.illustration-icon {
font-size: 4em;
text-align: center;
color: var(--muted-text-color);
opacity: 0.6;
margin-block: 1rem;
}
.illustration-logo {
width: 96px;
height: 96px;
margin: auto;
}
h1 {
font-size: 1.4em;
text-align: center;
}
h1 + p {
text-align: center;
color: var(--muted-text-color);
margin-bottom: 0;
}
.tooltip {
z-index: 15 !important;
}
}
body.setup.background-effects,
body.setup.background-effects .setup-container {
background: transparent;
}
/* macOS: draggable title bar region and traffic light buttons */
body.setup.platform-darwin {
.drag-region {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 40px;
-webkit-app-region: drag;
z-index: 10;
}
.back-button {
-webkit-app-region: no-drag;
z-index: 11;
}
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Slide transitions */
.slide-page {
position: absolute;
inset: 0;
}
.slide-out-forward,
.slide-out-backward,
.slide-in-forward,
.slide-in-backward {
animation-duration: 0.35s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
.slide-out-forward {
animation-name: slide-out-left;
}
.slide-out-backward {
animation-name: slide-out-right;
}
.slide-in-forward {
animation-name: slide-in-right;
}
.slide-in-backward {
animation-name: slide-in-left;
}
.page.select-language {
.dropdownWrapper {
padding-bottom: 2em;
width: 80%;
margin: auto;
}
.dropdownWrapper,
.dropdown,
.dropdown-menu {
height: 100%;
}
.dropdown-menu {
box-sizing: border-box;
overflow: auto;
}
}
.page.sync-from-desktop {
.card-columns {
display: flex;
flex-direction: row;
gap: 1.5rem;
}
.sync-from-desktop-waiting {
margin-top: 2rem;
text-align: center;
.main {
font-size: 1.35em;
}
.subtle {
color: var(--muted-text-color);
}
}
.ip-addresses {
min-width: 250px;
user-select: text;
display: flex;
flex-direction: column;
font-family: var(--monospace-font-family);
font-size: 0.9em;
.tn-card-body {
overflow: auto;
padding-bottom: 0.5em;
> :first-child {
font-weight: bold;
}
}
}
}
.page.sync-in-progress {
.sync-progress {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
progress {
width: 100%;
height: 1rem;
border-radius: 0.5rem;
overflow: hidden;
appearance: none;
&::-webkit-progress-bar {
background-color: var(--main-border-color);
}
&::-webkit-progress-value {
background-color: var(--main-text-color);
transition: width 0.2s ease-out;
}
}
span {
font-size: 0.85rem;
color: var(--muted-text-color);
min-width: 2.5em;
text-align: right;
}
}
}
@keyframes slide-out-left {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-100%); opacity: 0; }
}
@keyframes slide-out-right {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
@keyframes slide-in-right {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slide-in-left {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}

View File

@@ -1,204 +0,0 @@
import "jquery";
import utils from "./services/utils.js";
type SetupStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop" | "sync-from-server";
type SetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
class SetupController {
private step: SetupStep;
private setupType: SetupType = "";
private syncPollIntervalId: number | null = null;
private rootNode: HTMLElement;
private setupTypeForm: HTMLFormElement;
private syncFromServerForm: HTMLFormElement;
private setupTypeNextButton: HTMLButtonElement;
private setupTypeInputs: HTMLInputElement[];
private syncServerHostInput: HTMLInputElement;
private syncProxyInput: HTMLInputElement;
private passwordInput: HTMLInputElement;
private sections: Record<SetupStep, HTMLElement>;
constructor(rootNode: HTMLElement, syncInProgress: boolean) {
this.rootNode = rootNode;
this.step = syncInProgress ? "sync-in-progress" : "setup-type";
this.setupTypeForm = mustGetElement("setup-type-form", HTMLFormElement);
this.syncFromServerForm = mustGetElement("sync-from-server-form", HTMLFormElement);
this.setupTypeNextButton = mustGetElement("setup-type-next", HTMLButtonElement);
this.setupTypeInputs = Array.from(document.querySelectorAll<HTMLInputElement>("input[name='setup-type']"));
this.syncServerHostInput = mustGetElement("sync-server-host", HTMLInputElement);
this.syncProxyInput = mustGetElement("sync-proxy", HTMLInputElement);
this.passwordInput = mustGetElement("password", HTMLInputElement);
this.sections = {
"setup-type": mustGetElement("setup-type-section", HTMLElement),
"new-document-in-progress": mustGetElement("new-document-in-progress-section", HTMLElement),
"sync-from-desktop": mustGetElement("sync-from-desktop-section", HTMLElement),
"sync-from-server": mustGetElement("sync-from-server-section", HTMLElement),
"sync-in-progress": mustGetElement("sync-in-progress-section", HTMLElement)
};
}
init() {
this.setupTypeForm.addEventListener("submit", (event) => {
event.preventDefault();
void this.selectSetupType();
});
this.syncFromServerForm.addEventListener("submit", (event) => {
event.preventDefault();
void this.finish();
});
for (const input of this.setupTypeInputs) {
input.addEventListener("change", () => {
this.setupType = input.value as SetupType;
this.render();
});
}
for (const backButton of document.querySelectorAll<HTMLElement>("[data-action='back']")) {
backButton.addEventListener("click", () => {
this.back();
});
}
const serverAddress = `${location.protocol}//${location.host}`;
$("#current-host").html(serverAddress);
if (this.step === "sync-in-progress") {
this.startSyncPolling();
}
this.render();
this.rootNode.style.display = "";
}
private async selectSetupType() {
if (this.setupType === "new-document") {
this.setStep("new-document-in-progress");
await $.post("api/setup/new-document");
window.location.replace("./setup");
return;
}
if (this.setupType) {
this.setStep(this.setupType);
}
}
private back() {
this.setStep("setup-type");
this.setupType = "";
for (const input of this.setupTypeInputs) {
input.checked = false;
}
this.render();
}
private async finish() {
const syncServerHost = this.syncServerHostInput.value.trim();
const syncProxy = this.syncProxyInput.value.trim();
const password = this.passwordInput.value;
if (!syncServerHost) {
showAlert("Trilium server address can't be empty");
return;
}
if (!password) {
showAlert("Password can't be empty");
return;
}
// not using server.js because it loads too many dependencies
const resp = await $.post("api/setup/sync-from-server", {
syncServerHost,
syncProxy,
password
});
if (resp.result === "success") {
hideAlert();
this.setStep("sync-in-progress");
this.startSyncPolling();
} else {
showAlert(`Sync setup failed: ${resp.error}`);
}
}
private setStep(step: SetupStep) {
this.step = step;
this.render();
}
private render() {
for (const [step, section] of Object.entries(this.sections) as [SetupStep, HTMLElement][]) {
section.style.display = step === this.step ? "" : "none";
}
this.setupTypeNextButton.disabled = !this.setupType;
}
private getSelectedSetupType(): SetupType {
return (this.setupTypeInputs.find((input) => input.checked)?.value ?? "") as SetupType;
}
private startSyncPolling() {
if (this.syncPollIntervalId !== null) {
return;
}
this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000);
}
}
async function checkOutstandingSyncs() {
const { outstandingPullCount, initialized } = await $.get("api/sync/stats");
if (initialized) {
if (utils.isElectron()) {
const remote = utils.dynamicRequire("@electron/remote");
remote.app.relaunch();
remote.app.exit(0);
} else {
utils.reloadFrontendApp();
}
} else {
$("#outstanding-syncs").html(outstandingPullCount);
}
}
function showAlert(message: string) {
$("#alert").text(message);
$("#alert").show();
}
function hideAlert() {
$("#alert").hide();
}
function getSyncInProgress() {
const el = document.getElementById("syncInProgress");
if (!el || !(el instanceof HTMLMetaElement)) return false;
return !!parseInt(el.content);
}
function mustGetElement<T extends typeof HTMLElement>(id: string, ctor: T): InstanceType<T> {
const element = document.getElementById(id);
if (!element || !(element instanceof ctor)) {
throw new Error(`Expected element #${id}`);
}
return element as InstanceType<T>;
}
addEventListener("DOMContentLoaded", (event) => {
const rootNode = document.getElementById("setup-dialog");
if (!rootNode || !(rootNode instanceof HTMLElement)) return;
new SetupController(rootNode, getSyncInProgress()).init();
});

524
apps/client/src/setup.tsx Normal file
View File

@@ -0,0 +1,524 @@
import "./setup.css";
import { LOCALES, SetupSyncFromServerResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChildren, render } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { useTranslation } from "react-i18next";
import logo from "./assets/icon-color.svg?url";
import { initLocale, t } from "./services/i18n";
import server from "./services/server";
import { isElectron, replaceHtmlEscapedSlashes } from "./services/utils";
import ActionButton from "./widgets/react/ActionButton";
import Admonition from "./widgets/react/Admonition";
import Button from "./widgets/react/Button";
import { Card, CardFrame, CardSection } from "./widgets/react/Card";
import FormGroup from "./widgets/react/FormGroup";
import FormList, { FormListItem } from "./widgets/react/FormList";
import FormTextBox from "./widgets/react/FormTextBox";
import Icon from "./widgets/react/Icon";
async function main() {
await initLocale();
const bodyWrapper = document.createElement("div");
bodyWrapper.classList.add("setup-outer-wrapper");
document.body.classList.add("setup");
if (isElectron()) {
document.body.classList.add("electron", `platform-${window.process.platform}`, "background-effects");
}
render(<App />, bodyWrapper);
document.body.replaceChildren(bodyWrapper);
}
type State = "selectLanguage" | "firstOptions" | "createNewDocumentOptions" | "createNewDocumentWithDemo" | "createNewDocumentEmpty" | "syncFromDesktop" | "syncFromServer" | "syncFromServerInProgress" | "syncFromDesktopInProgress" | "syncFailed";
const STATE_ORDER: State[] = ["selectLanguage", "firstOptions", "createNewDocumentOptions", "createNewDocumentWithDemo", "createNewDocumentEmpty", "syncFromDesktop", "syncFromServer", "syncFromServerInProgress", "syncFromDesktopInProgress", "syncFailed"];
function renderState(state: State, setState: (state: State) => void) {
switch (state) {
case "selectLanguage": return <SelectLanguage setState={setState} />;
case "firstOptions": return <SetupOptions setState={setState} />;
case "createNewDocumentOptions": return <CreateNewDocumentOptions setState={setState} />;
case "createNewDocumentWithDemo": return <CreateNewDocumentInProgress withDemo />;
case "createNewDocumentEmpty": return <CreateNewDocumentInProgress />;
case "syncFromServer": return <SyncFromServer setState={setState} />;
case "syncFromDesktop": return <SyncFromDesktop setState={setState} />;
case "syncFromServerInProgress": return <SyncInProgress device="server" />;
case "syncFromDesktopInProgress": return <SyncInProgress device="desktop" />;
default: return null;
}
}
function App() {
const [state, setState] = useState<State>("selectLanguage");
const [prevState, setPrevState] = useState<State | null>(null);
const [transitioning, setTransitioning] = useState(false);
const prevStateRef = useRef<State>(state);
function handleSetState(newState: State) {
setPrevState(prevStateRef.current);
prevStateRef.current = newState;
setTransitioning(true);
setState(newState);
}
const direction = prevState !== null
? STATE_ORDER.indexOf(state) > STATE_ORDER.indexOf(prevState) ? "forward" : "backward"
: "forward";
return (
<div class="setup-container">
<div class="drag-region" />
{transitioning && prevState !== null && (
<div
class={`slide-page slide-out-${direction}`}
onAnimationEnd={() => {
setTransitioning(false);
setPrevState(null);
}}
>
{renderState(prevState, handleSetState)}
</div>
)}
<div class={`slide-page ${transitioning ? `slide-in-${direction}` : "slide-current"}`} key={state}>
{renderState(state, handleSetState)}
</div>
</div>
);
}
function SelectLanguage({ setState }: { setState: (state: State) => void }) {
const { t, i18n } = useTranslation();
const [ currentLocale, setCurrentLocale ] = useState(i18n.language);
const filteredLocales = useMemo(() => LOCALES.filter(l => !l.contentOnly), []);
return (
<SetupPage
title={t("setup.language")}
className="select-language"
illustration={<Icon icon="bx bx-globe" className="illustration-icon" />}
footer={<Button text={t("setup.continue")} kind="primary" onClick={() => setState("firstOptions")} />}
>
<FormList onSelect={async (id) => {
await i18n.changeLanguage(id);
setCurrentLocale(id);
}}>
{filteredLocales.map(locale => (
<FormListItem key={locale.id} value={locale.id} active={locale.id === currentLocale}>{locale.name}</FormListItem>
))}
</FormList>
</SetupPage>
);
}
function SetupOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
title={t("setup.heading")}
className="setup-options-container"
illustration={<img src={logo} alt="Setup illustration" className="illustration-logo" />}
onBack={() => setState("selectLanguage")}
>
<div class="setup-options">
<SetupOptionCard
icon="bx bx-file-blank"
title={t("setup.new-document")}
description={t("setup.new-document-description")}
onClick={() => setState("createNewDocumentOptions")}
/>
<SetupOptionCard
icon="bx bx-server"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-description")}
onClick={() => setState("syncFromServer")}
/>
<SetupOptionCard
icon="bx bx-desktop"
title={t("setup.sync-from-desktop")}
description={t("setup.sync-from-desktop-description")}
disabled={glob.isStandalone}
onClick={() => setState("syncFromDesktop")}
/>
</div>
</SetupPage>
);
}
type SyncStep = "connecting" | "syncing" | "finalizing";
function getSyncStep(stats: { outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }): SyncStep {
if (stats.initialized) {
return "finalizing"; // will reload momentarily
}
if (stats.totalPullCount !== null && stats.outstandingPullCount > 0) {
return "syncing";
}
if (stats.totalPullCount !== null && stats.outstandingPullCount === 0) {
return "finalizing";
}
return "connecting";
}
function SyncInProgress({ device }: { device: "server" | "desktop" }) {
const stats = useOutstandingSyncInfo();
const step = getSyncStep(stats);
useEffect(() => {
if (stats.initialized) {
onSetupFinished();
}
}, [stats.initialized]);
const steps: { key: SyncStep; label: string }[] = [
{ key: "connecting", label: t("setup.sync-step-connecting") },
{ key: "syncing", label: t("setup.sync-step-syncing") },
{ key: "finalizing", label: t("setup.sync-step-finalizing") }
];
const currentIndex = steps.findIndex((s) => s.key === step);
const syncingDone = currentIndex > steps.findIndex((s) => s.key === "syncing");
let progress = 0;
if (syncingDone) {
progress = 100;
} else if (stats.totalPullCount) {
progress = Math.round(((stats.totalPullCount - stats.outstandingPullCount) / stats.totalPullCount) * 100);
}
return (
<SetupPage
className="sync-in-progress"
illustration={<SyncIllustration targetDevice={device} />}
title={t("setup.sync-in-progress-title")}
>
<Card className="sync-steps">
{steps.map((s, i) => (
<CardSection className={i < currentIndex ? "completed" : i === currentIndex ? "active" : ""} key={s.key}>
<Icon icon={i < currentIndex ? "bx bx-check-circle" : i === currentIndex ? "bx bx-loader-circle bx-spin" : "bx bx-circle"} />{" "}
{s.label}
{s.key === "syncing" && (
<div class="sync-progress">
<progress value={syncingDone ? 1 : stats.totalPullCount! - stats.outstandingPullCount} max={syncingDone ? 1 : stats.totalPullCount!} />
<span>{progress}%</span>
</div>
)}
</CardSection>
))}
</Card>
</SetupPage>
);
}
function useOutstandingSyncInfo() {
const [ outstandingPullCount, setOutstandingPullCount ] = useState(0);
const [ totalPullCount, setTotalPullCount ] = useState<number | null>(null);
const [ initialized, setInitialized ] = useState(false);
async function refresh() {
const resp = await server.get<{ outstandingPullCount: number; totalPullCount: number | null; initialized: boolean }>("sync/stats");
setOutstandingPullCount(resp.outstandingPullCount);
setTotalPullCount(resp.totalPullCount);
setInitialized(resp.initialized);
}
useEffect(() => {
const interval = setInterval(refresh, 1000);
refresh();
return () => clearInterval(interval);
}, []);
return { outstandingPullCount, totalPullCount, initialized };
}
function CreateNewDocumentOptions({ setState }: { setState: (state: State) => void }) {
return (
<SetupPage
className="create-new-document-options"
title={t("setup.create-new-document-options-title")}
illustration={<Icon icon="bx bx-star" className="illustration-icon" />}
onBack={() => setState("firstOptions")}
>
<div class="setup-options">
<SetupOptionCard icon="bx bx-book-open" title={t("setup.create-new-document-options-with-demo")} description={t("setup.create-new-document-options-with-demo-description")} onClick={() => setState("createNewDocumentWithDemo")} />
<SetupOptionCard icon="bx bx-file-blank" title={t("setup.create-new-document-options-empty")} description={t("setup.create-new-document-options-empty-description")} onClick={() => setState("createNewDocumentEmpty")} />
</div>
</SetupPage>
);
}
function CreateNewDocumentInProgress({ withDemo = false }: { withDemo?: boolean }) {
useEffect(() => {
server.post(`setup/new-document${withDemo ? "" : "?skipDemoDb"}`).then(onSetupFinished);
}, [ withDemo ]);
return (
<SetupPage
className="create-new-document"
title={t("setup.create-new-document-title")}
description={t("setup.create-new-document-description")}
illustration={<Icon icon="bx bx-loader-circle bx-spin" className="illustration-icon" />}
/>
);
}
function SyncFromServer({ setState }: { setState: (state: State) => void }) {
const [ syncServerHost, setSyncServerHost ] = useState("");
const [ password, setPassword ] = useState("");
const [ syncProxy, setSyncProxy ] = useState("");
const [ error, setError ] = useState<string | null>(null);
const [ errorId, setErrorId ] = useState(0);
const [ isWrongPassword, setIsWrongPassword ] = useState(false);
const isValid = syncServerHost.trim() !== "" && password !== "";
function raiseError(message: string) {
setError(message);
setErrorId(id => id + 1);
}
async function handleFinishSetup() {
try {
const resp = await server.post<SetupSyncFromServerResponse>("setup/sync-from-server", {
syncServerHost: syncServerHost.trim(),
syncProxy: syncProxy.trim(),
password
});
if (resp.result === "success") {
setState("syncFromServerInProgress");
} else if (resp.error.includes("Incorrect password")) {
setIsWrongPassword(true);
} else {
raiseError(t("setup.sync-failed", { message: resp.error }));
}
} catch (e) {
raiseError(e instanceof Error ? e.message : String(e));
}
}
return (
<SetupPage
className="sync-from-server top-aligned"
title={t("setup.sync-from-server")}
description={t("setup.sync-from-server-page-description")}
illustration={<SyncIllustration targetDevice="server" />}
error={error}
errorId={errorId}
onBack={() => setState("firstOptions")}
footer={<Button text={t("setup.button-finish-setup")} kind="primary" onClick={handleFinishSetup} disabled={!isValid} />}
>
<form>
<Card>
<CardSection>
<FormGroup label={t("setup.server-host")} name="serverHost">
<FormTextBox
placeholder={t("setup.server-host-placeholder")}
currentValue={syncServerHost} onChange={setSyncServerHost}
autocomplete="trilium-sync-server-host"
required
/>
</FormGroup>
</CardSection>
<CardSection>
<FormGroup
label={t("setup.server-password")} name="serverPassword"
error={isWrongPassword ? t("setup.wrong-password") : undefined}
>
<FormTextBox
type="password"
currentValue={password} onChange={setPassword}
autocomplete="trilium-sync-server-password"
required
/>
</FormGroup>
</CardSection>
</Card>
<Card heading={t("setup.advanced-options")}>
<CardSection>
<FormGroup
name="proxyServer"
label={t("setup.proxy-server")}
description={isElectron() ? t("setup.proxy-instruction") : undefined}
>
<FormTextBox placeholder={t("setup.proxy-server-placeholder")} currentValue={syncProxy} onChange={setSyncProxy} />
</FormGroup>
</CardSection>
</Card>
</form>
</SetupPage>
);
}
function SyncFromDesktop({ setState }: { setState: (state: State) => void }) {
const networkAddresses = getNetworkAddresses();
useEffect(() => {
const interval = setInterval(async () => {
const status = await server.get<{ schemaExists: boolean }>("setup/status");
if (status.schemaExists) {
setState("syncFromDesktopInProgress");
}
}, 1000);
return () => clearInterval(interval);
}, [setState]);
return (
<SetupPage
className="sync-from-desktop"
title={t("setup.sync-from-desktop")}
illustration={<SyncIllustration targetDevice="desktop" />}
onBack={() => setState("firstOptions")}
>
<div class="card-columns">
<Card heading="On the other device">
<CardSection>1. {t("setup.sync-from-desktop-step1")}</CardSection>
<CardSection>2. {t("setup.sync-from-desktop-step2")}</CardSection>
<CardSection>3. {t("setup.sync-from-desktop-step3")}</CardSection>
<CardSection>4. {t("setup.sync-from-desktop-step4")}</CardSection>
<CardSection>5. {t("setup.sync-from-desktop-step5")}</CardSection>
</Card>
{networkAddresses.length > 0 && (
<Card heading={t("setup.your-ip-addresses")} className="ip-addresses">
{networkAddresses.map((addr) => (
<CardSection key={addr}>{addr}</CardSection>
))}
</Card>
)}
</div>
<div class="sync-from-desktop-waiting">
<div class="main"><Icon icon="bx bx-loader-circle bx-spin" />{" "} {t("setup.sync-from-desktop-waiting")}</div>
<div class="subtle">{t("setup.sync-from-desktop-warning")}</div>
</div>
</SetupPage>
);
}
function SyncIllustration({ targetDevice }: { targetDevice: "desktop" | "server" }) {
return (
<div class="sync-illustration">
<div>
<Icon icon={isElectron() ? "bx bx-desktop" : "bx bx-globe"} />
{t("setup.sync-illustration-this-device")}
</div>
<div class="sync-illustration-arrows" />
<div>
<Icon icon={targetDevice === "desktop" ? "bx bx-desktop" : "bx bx-server"} />
{targetDevice === "desktop" ? t("setup.sync-illustration-desktop-app") : t("setup.sync-illustration-server")}
</div>
</div>
);
}
function SetupOptionCard({ title, description, icon, onClick, disabled }: { title: string; description: string, icon: string, onClick?: () => void, disabled?: boolean }) {
return (
<CardFrame
className={clsx("setup-option-card", { disabled })}
onClick={disabled ? undefined : onClick}
>
<Icon icon={icon} />
<div>
<h3>{title}</h3>
<p>{description}</p>
</div>
</CardFrame>
);
}
function SetupPage({ title, description, className, illustration, children, footer, error, errorId, onBack }: {
title: string;
description?: string;
error?: string | null;
errorId?: number;
className?: string;
illustration?: ComponentChildren;
children?: ComponentChildren;
footer?: ComponentChildren;
onBack?: () => void;
}) {
const [ showError, setShowError ] = useState(!!error);
useEffect(() => {
if (error) {
setShowError(true);
}
}, [ error, errorId ]);
return (
<div className={clsx("page", className, { "contentless": !children })}>
{onBack && (
<Button
className="back-button"
icon="bx bx-arrow-back"
text={t("setup.button-back")}
onClick={onBack}
kind="lowProfile"
/>
)}
{error && showError && (
<Admonition className="page-error" type="caution">
<ActionButton icon="bx bx-x" text={t("setup.dismiss-error")} onClick={() => setShowError(false)} />
{replaceHtmlEscapedSlashes(error)}
</Admonition>
)}
{illustration}
<h1>{title}</h1>
{description && <p class="page-description">{description}</p>}
{children && <main>
{children}
</main>}
{footer && <footer>{footer}</footer>}
</div>
);
}
function getNetworkAddresses(): string[] {
if (!isElectron()) {
return [`${location.protocol}//${location.host}`];
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const os = require("os") as typeof import("os");
const interfaces = os.networkInterfaces();
const addresses: string[] = [];
for (const nets of Object.values(interfaces)) {
if (!nets) continue;
for (const net of nets) {
if (net.internal) continue;
if (net.family === "IPv6" && net.scopeid !== 0) continue;
addresses.push(net.address);
}
}
// Sort by likelihood of being the local network address.
addresses.sort((a, b) => networkScore(a) - networkScore(b));
return addresses.map((addr) => `${location.protocol}//${addr}:${location.port}`);
}
function networkScore(addr: string): number {
if (addr.startsWith("192.168.")) return 0;
if (addr.startsWith("10.")) return 1;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(addr)) return 2;
if (addr.includes(":")) return 4; // IPv6
return 3;
}
function onSetupFinished() {
if (isElectron()) {
// On Electron we need to use the setup route because it handles the closing of the setup window and opening the main app window.
location.href = "setup";
} else {
location.reload();
}
}
main();

View File

@@ -1750,11 +1750,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
justify-content: space-between;
align-items: baseline;
font-weight: bold;
color: var(--muted-text-color) !important;
}
#right-pane .card-header-title {
text-transform: uppercase;
color: var(--muted-text-color) !important;
}
#right-pane .card-header-buttons {
@@ -2641,26 +2638,3 @@ iframe.print-iframe {
min-height: 50px;
align-items: center;
}
.ocr-text-section {
padding: 10px;
background: var(--accented-background-color);
border-left: 3px solid var(--main-border-color);
text-align: left;
width: 100%;
}
.ocr-header {
font-weight: bold;
margin-bottom: 8px;
font-size: 0.9em;
color: var(--muted-text-color);
}
.ocr-content {
max-height: 150px;
overflow-y: auto;
font-size: 0.9em;
line-height: 1.4;
white-space: pre-wrap;
}

View File

@@ -0,0 +1,11 @@
/* 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

@@ -544,11 +544,14 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
vertical-align: middle;
}
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
margin: 0 0 0 12px;
}
#toast-container .toast.no-title {
flex-direction: row;
}
#toast-container .toast .toast-body {
flex-grow: 1;
overflow: hidden;

View File

@@ -26,8 +26,7 @@
.modal .modal-header .btn-close,
.modal .modal-header .help-button,
.modal .modal-header .custom-title-bar-button,
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
display: flex;
justify-content: center;
align-items: center;
@@ -47,14 +46,12 @@
}
.modal .modal-header .btn-close,
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
#toast-container .toast .toast-header .btn-close {
--modal-control-button-hover-background: var(--modal-close-button-hover-background);
}
.modal .modal-header .btn-close::after,
#toast-container .toast .toast-header .btn-close::after,
#toast-container .toast .toast-close .btn-close::after {
#toast-container .toast .toast-header .btn-close::after {
content: "\ec8d";
font-family: boxicons;
}
@@ -70,8 +67,7 @@
.modal .modal-header .btn-close:hover,
.modal .modal-header .help-button:hover,
.modal .modal-header .custom-title-bar-button:hover,
#toast-container .toast .toast-header .btn-close:hover,
#toast-container .toast .toast-close .btn-close:hover {
#toast-container .toast .toast-header .btn-close:hover {
background: var(--modal-control-button-hover-background);
color: var(--modal-control-button-hover-color);
}
@@ -79,22 +75,19 @@
.modal .modal-header .btn-close:active,
.modal .modal-header .help-button:active,
.modal .modal-header .custom-title-bar-button:active,
#toast-container .toast .toast-header .btn-close:active,
#toast-container .toast .toast-close .btn-close:active {
#toast-container .toast .toast-header .btn-close:active {
transform: scale(.85);
}
.modal .modal-header .btn-close:focus,
.modal .modal-header .help-button:focus,
#toast-container .toast .toast-header .btn-close:focus,
#toast-container .toast .toast-close .btn-close:focus {
#toast-container .toast .toast-header .btn-close:focus {
box-shadow: none !important;
}
.modal .modal-header .btn-close:focus-visible,
.modal .modal-header .help-button:focus-visible,
#toast-container .toast .toast-header .btn-close:focus-visible,
#toast-container .toast .toast-close .btn-close:focus-visible {
#toast-container .toast .toast-header .btn-close:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
outline-offset: 2px;
}

View File

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

View File

@@ -709,8 +709,7 @@
"advanced": "高级",
"export_as_image": "导出为图像",
"export_as_image_png": "PNG栅格",
"export_as_image_svg": "SVG矢量图",
"view_ocr_text": "查看 OCR 文本"
"export_as_image_svg": "SVG矢量图"
},
"onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@@ -1198,28 +1197,12 @@
},
"images": {
"images_section_title": "图片",
"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}}"
"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": "像素"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "附件清理超时",
@@ -1437,6 +1420,9 @@
"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": {
@@ -1549,9 +1535,8 @@
"new-feature": "新建",
"collections": "集合",
"book": "集合",
"ai-chat": "AI对话",
"spreadsheet": "电子表格",
"llm-chat": "AI对话"
"ai-chat": "AI聊天",
"spreadsheet": "电子表格"
},
"protect_note": {
"toggle-on": "保护笔记",
@@ -2061,9 +2046,7 @@
"title": "实验选项",
"disclaimer": "这些选项处于实验阶段,可能导致系统不稳定。请谨慎使用。",
"new_layout_name": "新布局",
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。",
"llm_name": "AI/大语言模型对话",
"llm_description": "启用由大语言模型驱动的 AI对话侧边栏和大语言模型对话笔记。"
"new_layout_description": "尝试全新布局,呈现更现代的外观并提升易用性。后续版本将进行重大调整。"
},
"tab_history_navigation_buttons": {
"go-back": "返回前一笔记",
@@ -2232,77 +2215,5 @@
"sample_venn": "韦恩图",
"sample_ishikawa": "鱼骨图",
"placeholder": "输入你的美人鱼图的内容,或者使用下面的示例图之一。"
},
"llm_chat": {
"placeholder": "输入消息…",
"send": "发送",
"sending": "正在发送...",
"empty_state": "在下方输入消息,即可开始对话。",
"searching_web": "在网上搜索…",
"web_search": "联网搜索",
"sources": "来源",
"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": "未配置人工智能提供商。添加一个即可开始对话。",
"add_provider": "添加人工智能提供商",
"note_tools": "笔记访问"
},
"sidebar_chat": {
"title": "AI对话",
"launcher_title": "打开AI对话",
"new_chat": "开始新对话",
"save_chat": "将对话保存到笔记",
"empty_state": "开始对话",
"history": "对话历史",
"recent_chats": "最近对话",
"no_chats": "无历史对话"
},
"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识别已开始。请稍候片刻并刷新页面。",
"processing_failed": "OCR处理启动失败",
"view_extracted_text": "查看提取的文本OCR"
},
"mind-map": {
"addChild": "添加子节点",
"addParent": "添加父节点",
"addSibling": "添加同级节点",
"removeNode": "删除节点",
"focus": "专注模式",
"cancelFocus": "退出专注模式",
"moveUp": "上移",
"moveDown": "下移",
"link": "链接",
"linkBidirectional": "双向链接",
"clickTips": "请点击目标节点",
"summary": "总结"
},
"llm": {
"settings_description": "配置人工智能和大语言模型集成。",
"add_provider": "添加提供商"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1386,6 +1386,9 @@
"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

@@ -369,7 +369,7 @@
"calendar_root": "marks note which should be used as root for day notes. Only one should be marked as such.",
"archived": "notes with this label won't be visible by default in search results (also in Jump To, Add Link dialogs etc).",
"exclude_from_export": "notes (with their sub-tree) won't be included in any note export",
"run": "defines on which events script should run. Possible values are:\n<ul>\n<li>frontendStartup - when Trilium frontend starts up (or is refreshed), but not on mobile.</li>\n<li>mobileStartup - when Trilium frontend starts up (or is refreshed), on mobile.</li>\n<li>backendStartup - when Trilium backend starts up.</li>\n<li>hourly - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour.</li>\n<li>daily - run once a day.</li>\n</ul>",
"run": "defines on which events script should run. Possible values are:\n<ul>\n<li>frontendStartup - when Trilium frontend starts up (or is refreshed), but not on mobile.</li>\n<li>mobileStartup - when Trilium frontend starts up (or is refreshed), on mobile.</li>\n<li>backendStartup - when Trilium backend starts up</li>\n<li>hourly - run once an hour. You can use additional label <code>runAtHour</code> to specify at which hour.</li>\n<li>daily - run once a day</li>\n</ul>",
"run_on_instance": "Define which trilium instance should run this on. Default to all instances.",
"run_at_hour": "On which hour should this run. Should be used together with <code>#run=hourly</code>. Can be defined multiple times for more runs during the day.",
"disable_inclusion": "scripts with this label won't be included into parent script execution.",
@@ -691,7 +691,6 @@
"search_in_note": "Search in note",
"note_source": "Note source",
"note_attachments": "Note attachments",
"view_ocr_text": "View OCR text",
"open_note_externally": "Open note externally",
"open_note_externally_title": "File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.",
"open_note_custom": "Open note custom",
@@ -1074,7 +1073,6 @@
"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):",
@@ -1159,9 +1157,7 @@
"title": "Experimental Options",
"disclaimer": "These options are experimental and may cause instability. Use with caution.",
"new_layout_name": "New Layout",
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases.",
"llm_name": "AI / LLM Chat",
"llm_description": "Enable the AI chat sidebar and LLM chat notes powered by large language models."
"new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases."
},
"fonts": {
"theme_defined": "Theme defined",
@@ -1256,28 +1252,12 @@
},
"images": {
"images_section_title": "Images",
"download_images_automatically": "Download images automatically",
"download_images_description": "Download referenced online images from pasted HTML so they are available offline.",
"enable_image_compression": "Image compression",
"enable_image_compression_description": "Compress and resize images when they are uploaded or pasted.",
"max_image_dimensions": "Max image dimensions",
"max_image_dimensions_description": "Images exceeding this size will be resized automatically.",
"download_images_automatically": "Download images automatically for offline use.",
"download_images_description": "Pasted HTML can contain references to online images, Trilium will find those references and download the images so that they are available offline.",
"enable_image_compression": "Enable image compression",
"max_image_dimensions": "Max width / height of an image (image will be resized if it exceeds this setting).",
"max_image_dimensions_unit": "pixels",
"jpeg_quality": "JPEG quality",
"jpeg_quality_description": "Recommended range is 5085. Lower values reduce file size, higher values preserve detail.",
"ocr_section_title": "Text Extraction (OCR)",
"ocr_related_content_languages": "Content languages (used for text extraction)",
"ocr_auto_process": "Auto-process new files",
"ocr_auto_process_description": "Automatically extract text from newly uploaded or pasted files.",
"ocr_min_confidence": "Minimum confidence",
"ocr_confidence_description": "Only extract text above this confidence threshold. Lower values include more text but may be less accurate.",
"batch_ocr_title": "Process Existing Files",
"batch_ocr_description": "Extract text from all existing images, PDFs, and Office documents in your notes. This may take some time depending on the number of files.",
"batch_ocr_start": "Start Batch Processing",
"batch_ocr_starting": "Starting batch processing...",
"batch_ocr_progress": "Processing {{processed}} of {{total}} files...",
"batch_ocr_completed": "Batch processing completed! Processed {{processed}} files.",
"batch_ocr_error": "Error during batch processing: {{error}}"
"jpeg_quality_description": "JPEG quality (10 - worst quality, 100 - best quality, 50 - 85 is recommended)"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Attachment Erasure Timeout",
@@ -1323,7 +1303,7 @@
"custom_name_label": "Custom search engine name",
"custom_name_placeholder": "Customize search engine name",
"custom_url_label": "Custom search engine URL should include {keyword} as a placeholder for the search term.",
"custom_url_placeholder": "Customize search engine URL",
"custom_url_placeholder": "Customize search engine url",
"save_button": "Save"
},
"tray": {
@@ -1499,15 +1479,12 @@
"spellcheck": {
"title": "Spell Check",
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
"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."
"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."
},
"sync_2": {
"config_title": "Sync Configuration",
@@ -1622,7 +1599,6 @@
"geo-map": "Geo Map",
"beta-feature": "Beta",
"ai-chat": "AI Chat",
"llm-chat": "AI Chat",
"task-list": "Task List",
"new-feature": "New",
"collections": "Collections",
@@ -1634,48 +1610,6 @@
"toggle-on-hint": "Note is not protected, click to make it protected",
"toggle-off-hint": "Note is protected, click to make it unprotected"
},
"llm_chat": {
"placeholder": "Type a message...",
"send": "Send",
"sending": "Sending...",
"empty_state": "Start a conversation by typing a message below.",
"searching_web": "Searching the web...",
"web_search": "Web search",
"note_tools": "Note access",
"sources": "Sources",
"sources_summary": "{{count}} sources from {{sites}} sites",
"extended_thinking": "Extended thinking",
"legacy_models": "Legacy models",
"thinking": "Thinking...",
"thought_process": "Thought process",
"tool_calls": "{{count}} tool call(s)",
"input": "Input",
"result": "Result",
"error": "Error",
"tool_error": "failed",
"total_tokens": "{{total}} tokens",
"tokens_detail": "{{prompt}} prompt + {{completion}} completion",
"tokens_used": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens",
"tokens_used_with_cost": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} prompt + {{completion}} completion = {{total}} tokens",
"tokens_used_with_model_and_cost": "{{model}}: {{prompt}} prompt + {{completion}} completion = {{total}} tokens (~${{cost}})",
"tokens": "tokens",
"context_used": "{{percentage}}% used",
"note_context_enabled": "Click to disable note context: {{title}}",
"note_context_disabled": "Click to include current note in context",
"no_provider_message": "No AI provider configured. Add one to start chatting.",
"add_provider": "Add AI Provider"
},
"sidebar_chat": {
"title": "AI Chat",
"launcher_title": "Open AI Chat",
"new_chat": "Start new chat",
"save_chat": "Save chat to notes",
"empty_state": "Start a conversation",
"history": "Chat history",
"recent_chats": "Recent chats",
"no_chats": "No previous chats"
},
"shared_switch": {
"shared": "Shared",
"toggle-on-title": "Share the note",
@@ -1987,7 +1921,7 @@
},
"content_language": {
"title": "Content languages",
"description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking, right-to-left support and text extraction (OCR)."
"description": "Select one or more languages that should appear in the language selection in the Basic Properties section of a read-only or editable text note. This will allow features such as spell-checking or right-to-left support."
},
"switch_layout_button": {
"title_vertical": "Move editing pane to the bottom",
@@ -2087,22 +2021,6 @@
"calendar_view": {
"delete_note": "Delete note..."
},
"ocr": {
"extracted_text": "Extracted Text (OCR)",
"extracted_text_title": "Extracted Text (OCR)",
"loading_text": "Loading OCR text...",
"no_text_available": "No OCR text available",
"no_text_explanation": "This note has not been processed for OCR text extraction or no text was found.",
"failed_to_load": "Failed to load OCR text",
"process_now": "Process OCR",
"processing": "Processing...",
"processing_started": "OCR processing has been started. Please wait a moment and refresh.",
"processing_complete": "OCR processing complete.",
"processing_failed": "Failed to start OCR processing",
"text_filtered_low_confidence": "OCR detected text with {{confidence}}% confidence, but it was discarded because your minimum threshold is {{threshold}}%.",
"open_media_settings": "Open Settings",
"view_extracted_text": "View extracted text (OCR)"
},
"command_palette": {
"tree-action-name": "Tree: {{name}}",
"export_note_title": "Export Note",
@@ -2311,63 +2229,57 @@
"sample_user_journey": "User Journey",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa",
"sample_treeview": "TreeView",
"sample_wardley": "Wardley Map"
"sample_ishikawa": "Ishikawa"
},
"mind-map": {
"addChild": "Add child",
"addParent": "Add parent",
"addSibling": "Add sibling",
"removeNode": "Remove node",
"focus": "Focus Mode",
"cancelFocus": "Cancel Focus Mode",
"moveUp": "Move up",
"moveDown": "Move down",
"link": "Link",
"linkBidirectional": "Bidirectional Link",
"clickTips": "Please click the target node",
"summary": "Summary"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "Configure AI and Large Language Model integrations.",
"feature_not_enabled": "Enable the LLM experimental feature in Settings → Advanced → Experimental features to use AI integrations.",
"add_provider": "Add Provider",
"add_provider_title": "Add AI Provider",
"configured_providers": "Configured Providers",
"no_providers_configured": "No providers configured yet.",
"provider_name": "Name",
"provider_type": "Provider",
"actions": "Actions",
"delete_provider": "Delete",
"delete_provider_confirmation": "Are you sure you want to delete the provider \"{{name}}\"?",
"api_key": "API Key",
"api_key_placeholder": "Enter your API key",
"cancel": "Cancel",
"mcp_title": "MCP (Model Context Protocol)",
"mcp_enabled": "MCP server",
"mcp_enabled_description": "Expose a Model Context Protocol (MCP) endpoint so that AI coding assistants (e.g. Claude Code, GitHub Copilot) can read and modify your notes. The endpoint is only accessible from localhost.",
"mcp_endpoint_title": "Endpoint URL",
"mcp_endpoint_description": "Add this URL to your AI assistant's MCP configuration",
"tools": {
"search_notes": "Search notes",
"get_note": "Get note",
"get_note_content": "Get note content",
"update_note_content": "Update note content",
"append_to_note": "Append to note",
"create_note": "Create note",
"get_attributes": "Get attributes",
"get_attribute": "Get attribute",
"set_attribute": "Set attribute",
"delete_attribute": "Delete attribute",
"get_child_notes": "Get child notes",
"get_subtree": "Get subtree",
"load_skill": "Load skill",
"web_search": "Web search",
"note_in_parent": "<Note/> in <Parent/>",
"get_attachment": "Get attachment",
"get_attachment_content": "Read attachment content"
}
"setup": {
"heading": "Get started with Trilium",
"new-document": "New knowledge base",
"new-document-description": "Start with a clean knowledge base and begin right away.",
"sync-from-desktop": "Connect a desktop app",
"sync-from-desktop-description": "You only have a Trilium desktop app running on another device. This device will sync its data from that desktop app.",
"sync-from-server": "Connect to an existing server",
"sync-from-server-description": "You have a Trilium server running elsewhere (either self-hosted or in the cloud). This device will sync its data from that server.",
"next": "Next",
"init-in-progress": "Document initialization in progress",
"redirecting": "You will be shortly redirected to the application.",
"title": "Setup",
"sync-from-server-page-description": "Enter your server details below to connect your existing workspace.",
"sync-in-progress-title": "Sync in progress",
"sync-in-progress-description": "Your device is now connected and items are being synchronized.",
"button-back": "Back",
"button-finish-setup": "Finish setup",
"sync-step-connecting": "Connecting to server",
"sync-step-syncing": "Syncing data",
"sync-step-finalizing": "Setting up options",
"create-new-document-options-title": "How would you like to start?",
"create-new-document-options-with-demo": "With demo content",
"create-new-document-options-with-demo-description": "Explore Trilium with example content.",
"create-new-document-options-empty": "Empty",
"create-new-document-options-empty-description": "Start with a blank knowledge base. You can import demo notes later.",
"create-new-document-title": "Preparing your knowledge base",
"create-new-document-description": "This will only take a moment.",
"sync-illustration-this-device": "This device",
"sync-illustration-desktop-app": "Your desktop app",
"sync-illustration-server": "Your server",
"sync-from-desktop-step1": "Open your desktop instance of Trilium Notes.",
"sync-from-desktop-step2": "From the Trilium Menu, click Options.",
"sync-from-desktop-step3": "Click on Sync category in the note tree.",
"sync-from-desktop-step4": "Change server instance address to point to one of the addresses on the right and click Save.",
"sync-from-desktop-step5": "Click the \"Test sync\" button to verify connection is successful.",
"sync-from-desktop-warning": "Make sure both devices are on the same network.",
"sync-from-desktop-waiting": "Waiting for connection...",
"advanced-options": "Advanced options",
"sync-failed": "Failed to sync: {{message}}",
"server-host": "Trilium server address",
"server-host-placeholder": "https://<hostname>:<port>",
"server-password": "Password",
"proxy-server": "Proxy server (optional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"proxy-instruction": "If you leave proxy setting blank, system proxy will be used.",
"dismiss-error": "Dismiss error",
"wrong-password": "Incorrect password. Please try again.",
"language": "Language",
"continue": "Continue",
"your-ip-addresses": "Addresses for this device"
}
}

View File

@@ -1432,6 +1432,9 @@
"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

@@ -28,10 +28,7 @@
},
"widget-render-error": {
"title": "Rendu impossible d'un widget React custom"
},
"widget-missing-parent": "Le widget personnalisé ne comprend pas de propriété '{{property}}' définie\n\nSi ce script est prévu pour être exécuté sans fonctionnalité UI, utilisez '#run=frontendStartup' plutôt.",
"open-script-note": "Ouvrir une note script",
"scripting-error": "Échec du script personnalisé : {{title}}"
}
},
"add_link": {
"add_link": "Ajouter un lien",
@@ -49,7 +46,7 @@
"prefix": "Préfixe : ",
"save": "Sauvegarder",
"branch_prefix_saved": "Le préfixe de la branche a été enregistré.",
"edit_branch_prefix_multiple": "Modifier le préfixe pour {{count}} branches",
"edit_branch_prefix_multiple": "Modifier le préfixe de branche pour {{count}} branches",
"branch_prefix_saved_multiple": "Le préfixe de la branche a été sauvegardé pour {{count}} branches.",
"affected_branches": "Branches impactées ({{count}}):"
},
@@ -117,7 +114,7 @@
"export_in_progress": "Exportation en cours : {{progressCount}}",
"export_finished_successfully": "L'exportation s'est terminée avec succès.",
"format_pdf": "PDF - pour l'impression ou le partage de documents.",
"share-format": "HTML pour la publication Web : utilise le même thème que celui utilisé pour les notes partagées, mais peut être publié sous forme de site Web statique."
"share-format": "HTML pour la publication Web - utilise le même thème que celui utilisé pour les notes partagées, mais peut être publié sous forme de site Web statique."
},
"help": {
"noteNavigation": "Navigation dans les notes",
@@ -446,8 +443,7 @@
"and_more": "... et {{count}} plus.",
"print_landscape": "Lors de l'exportation en PDF, change l'orientation de la page en paysage au lieu de portrait.",
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
"color_type": "Couleur",
"textarea": "Texte multiligne"
"color_type": "Couleur"
},
"attribute_editor": {
"help_text_body1": "Pour ajouter un label, tapez simplement par ex. <code>#rock</code>, ou si vous souhaitez également ajouter une valeur, tapez par ex. <code>#année = 2020</code>",
@@ -663,8 +659,7 @@
"show-cheatsheet": "Afficher l'aide rapide",
"toggle-zen-mode": "Zen Mode",
"new-version-available": "Nouvelle mise à jour disponible",
"download-update": "Obtenir la version {{latestVersion}}",
"search_notes": "Rechercher notes"
"download-update": "Obtenir la version {{latestVersion}}"
},
"zen_mode": {
"button_exit": "Sortir du Zen mode"
@@ -708,8 +703,7 @@
"advanced": "Avancé",
"export_as_image": "Exporter en tant qu'image",
"export_as_image_png": "PNG",
"export_as_image_svg": "SVG (vectoriel)",
"note_map": "Note Carte"
"export_as_image_svg": "SVG (vectoriel)"
},
"onclick_button": {
"no_click_handler": "Le widget bouton '{{componentId}}' n'a pas de gestionnaire de clic défini"
@@ -747,25 +741,23 @@
"button_title": "Exporter le diagramme au format SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Créer une note enfant et l'ajouter à la carte",
"create_child_note_title": "Créer une nouvelle note enfant et l'ajouter à cette carte de relation",
"reset_pan_zoom_title": "Réinitialiser le panoramique et le zoom aux coordonnées et à la position initiales",
"zoom_in_title": "Zoomer",
"zoom_out_title": "Zoom arrière"
},
"zpetne_odkazy": {
"relation": "relation",
"backlink_one": "{{count}} Rétrolien",
"backlink_many": "{{count}} Rétroliens",
"backlink_other": "{{count}} Rétrolien"
"backlink_one": "{{count}} Lien inverse",
"backlink_many": "",
"backlink_other": "{{count}} Liens inverses"
},
"mobile_detail_menu": {
"insert_child_note": "Insérer une note enfant",
"delete_this_note": "Supprimer cette note",
"error_cannot_get_branch_id": "Impossible d'obtenir branchId pour notePath '{{notePath}}'",
"error_unrecognized_command": "Commande non reconnue {{command}}",
"note_revisions": "Révision de la note",
"backlinks": "Rétro-liens",
"content_language_switcher": "Langue du contenu: {{language}}"
"note_revisions": "Révision de la note"
},
"note_icon": {
"change_note_icon": "Changer l'icône de note",
@@ -774,12 +766,7 @@
"filter": "Filtre",
"filter-none": "Toutes les icônes",
"filter-default": "Icônes par défaut",
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}",
"no_results": "Aucune icône trouvée.",
"search_placeholder_one": "{{number}} icône recherchées parmi {{count}} packs.",
"search_placeholder_many": "{{number}} icônes recherchées parmi {{count}} packs.",
"search_placeholder_other": "{{number}} icônes recherchées parmi {{count}} packs.",
"search_placeholder_filtered": "Rechercher {{number}} icônes dans {{name}}"
"icon_tooltip": "{{name}}\nPack d'icônes : {{iconPack}}"
},
"basic_properties": {
"note_type": "Type de note",
@@ -795,7 +782,7 @@
"collapse_all_notes": "Réduire toutes les notes",
"collapse": "Réduire",
"expand": "Développer",
"invalid_view_type": "Type de vue '{{type}}' non valide",
"invalid_view_type": "Type de vue non valide '{{type}}'",
"calendar": "Calendrier",
"book_properties": "Propriétés de la collection",
"table": "Tableau",
@@ -806,8 +793,7 @@
"expand_tooltip": "Développe les éléments enfants directs de cette collection (à un niveau). Pour plus d'options, appuyez sur la flèche à droite.",
"expand_first_level": "Développer les enfants directs",
"expand_nth_level": "Développer sur {{depth}} niveaux",
"expand_all_levels": "Développer tous les niveaux",
"hide_child_notes": "Masquer les notes enfants dans larborescence"
"expand_all_levels": "Développer tous les niveaux"
},
"edited_notes": {
"no_edited_notes_found": "Aucune note modifiée ce jour-là...",
@@ -820,7 +806,7 @@
"file_type": "Type de fichier",
"file_size": "Taille du fichier",
"download": "Télécharger",
"open": "Ouvrir dans une nouvelle fenêtre",
"open": "Ouvrir",
"upload_new_revision": "Téléverser une nouvelle version",
"upload_success": "Une nouvelle version de fichier a été téléversée.",
"upload_failed": "Le téléversement d'une nouvelle version de fichier a échoué.",
@@ -840,8 +826,7 @@
},
"inherited_attribute_list": {
"title": "Attributs hérités",
"no_inherited_attributes": "Aucun attribut hérité.",
"none": "aucun"
"no_inherited_attributes": "Aucun attribut hérité."
},
"note_info_widget": {
"note_id": "Identifiant de la note",
@@ -918,8 +903,7 @@
"unknown_search_option": "Option de recherche inconnue {{searchOptionName}}",
"search_note_saved": "La note de recherche a été enregistrée dans {{- notePathTitle}}",
"actions_executed": "Les actions ont été exécutées.",
"view_options": "Afficher les options:",
"option": "option"
"view_options": "Afficher les options:"
},
"similar_notes": {
"title": "Notes similaires",
@@ -1013,7 +997,7 @@
"no_attachments": "Cette note ne contient aucune pièce jointe."
},
"book": {
"no_children_help": "Cette collection ne contient pas de notes enfants, il n'y a donc rien à afficher.",
"no_children_help": "Cette note de type Livre n'a aucune note enfant, donc il n'y a rien à afficher. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> pour plus de détails.",
"drag_locked_title": "Edition verrouillée",
"drag_locked_message": "Le glisser-déposer n'est pas autorisé car l'édition de cette collection est verrouillé."
},
@@ -1187,8 +1171,8 @@
},
"code_mime_types": {
"title": "Types MIME disponibles dans la liste déroulante",
"tooltip_syntax_highlighting": "Mise en évidence de la syntaxe",
"tooltip_code_block_syntax": "Blocs de code dans les notes textuelles",
"tooltip_syntax_highlighting": "Souligner la syntaxe",
"tooltip_code_block_syntax": "Blocs de code dans les notes de texte",
"tooltip_code_note_syntax": "Notes de code"
},
"vim_key_bindings": {
@@ -1383,14 +1367,16 @@
"description": "Description",
"reload_app": "Recharger l'application pour appliquer les modifications",
"set_all_to_default": "Réinitialiser aux valeurs par défaut",
"confirm_reset": "Voulez-vous vraiment réinitialiser tous les raccourcis clavier par défaut ?",
"no_results": "Aucun raccourci correspondant à '{{filter}}'"
"confirm_reset": "Voulez-vous vraiment réinitialiser tous les raccourcis clavier par défaut ?"
},
"spellcheck": {
"title": "Vérification orthographique",
"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": {
@@ -1416,7 +1402,7 @@
"will_be_deleted_in": "Cette pièce jointe sera automatiquement supprimée dans {{time}}",
"will_be_deleted_soon": "Cette pièce jointe sera bientôt supprimée automatiquement",
"deletion_reason": ", car la pièce jointe n'est pas liée dans le contenu de la note. Pour empêcher la suppression, ajoutez à nouveau le lien de la pièce jointe dans le contenu d'une note ou convertissez la pièce jointe en note.",
"role_and_size": "Rôle : {{role}}, Taille : {{size}}, MIME: {{- mimeType}}",
"role_and_size": "Rôle : {{role}}, Taille : {{size}}",
"link_copied": "Lien de pièce jointe copié dans le presse-papiers.",
"unrecognized_role": "Rôle de pièce jointe « {{role}} » non reconnu."
},
@@ -1467,13 +1453,10 @@
"import-into-note": "Importer dans la note",
"apply-bulk-actions": "Appliquer des Actions groupées",
"converted-to-attachments": "Les notes {{count}} ont été converties en pièces jointes.",
"convert-to-attachment-confirm": "Êtes-vous sûr de vouloir convertir les notes sélectionnées en pièces jointes de leurs notes parentales? Cette opération s'applique uniquement aux notes d'image, les autres notes seront ignorées.",
"convert-to-attachment-confirm": "Êtes-vous sûr de vouloir convertir les notes sélectionnées en pièces jointes de leurs notes parentes ?",
"archive": "Archive",
"unarchive": "Désarchiver",
"open-in-popup": "Modification rapide",
"open-in-a-new-window": "Ouvrir dans une nouvelle fenêtre",
"hide-subtree": "Masquer le sous-arbre",
"show-subtree": "Afficher le sous-arbre"
"open-in-popup": "Modification rapide"
},
"shared_info": {
"shared_publicly": "Cette note est partagée publiquement sur {{- link}}.",
@@ -1502,10 +1485,7 @@
"task-list": "Liste de tâches",
"book": "Collection",
"new-feature": "Nouveau",
"collections": "Collections",
"ai-chat": "Chat IA",
"llm-chat": "Chat AI",
"spreadsheet": "Feuille de calcul"
"collections": "Collections"
},
"protect_note": {
"toggle-on": "Protéger la note",
@@ -1536,13 +1516,7 @@
},
"highlights_list_2": {
"title": "Accentuations",
"options": "Options",
"title_with_count_one": "{{count}} mise en évidence",
"title_with_count_many": "{{count}} mises en évidence",
"title_with_count_other": "{{count}} mises en évidence",
"modal_title": "Configurer les mises en évidence",
"menu_configure": "Configuration des mises en évidence...",
"no_highlights": "Aucune mise en évidence."
"options": "Options"
},
"quick-search": {
"placeholder": "Recherche rapide",
@@ -1566,17 +1540,7 @@
"create-child-note": "Créer une note enfant",
"unhoist": "Désactiver le focus",
"toggle-sidebar": "Basculer la barre latérale",
"dropping-not-allowed": "Déplacer des notes à cet emplacement n'est pas autorisé.",
"clone-indicator-tooltip": "Cette note a {{- count}} parents: {{- parents}}",
"clone-indicator-tooltip-single": "Cette note est clonée (1 parent supplémentaire: {{- parent}})",
"shared-indicator-tooltip": "Cette note est partagée publiquement",
"shared-indicator-tooltip-with-url": "Cette note est partagée publiquement sur: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} note enfant cachée de l'arbre",
"subtree-hidden-tooltip_many": "{{count}} notes enfants cachées de l'arbre",
"subtree-hidden-tooltip_other": "{{count}} notes enfants cachées de l'arbre",
"subtree-hidden-moved-title": "Ajouté à {{title}}",
"subtree-hidden-moved-description-collection": "Cette collection cache ses notes enfants dans l'arbre.",
"subtree-hidden-moved-description-other": "Les notes enfants sont cachées dans l'arbre pour cette note."
"dropping-not-allowed": "Lâcher des notes à cet endroit n'est pas autorisé"
},
"title_bar_buttons": {
"window-on-top": "Épingler cette fenêtre au premier plan"
@@ -1587,12 +1551,7 @@
"printing_pdf": "Export au format PDF en cours...",
"print_report_title": "Imprimer le rapport",
"print_report_collection_details_button": "Consulter les détails",
"print_report_collection_details_ignored_notes": "Notes ignorées",
"print_report_error_title": "Échec de l'impression",
"print_report_stack_trace": "Trace de la pile",
"print_report_collection_content_one": "La {{count}} note de la collection n'a pas pu être imprimée car elle n'est pas prises en charge ou est protégée.",
"print_report_collection_content_many": "Les {{count}} notes de la collection n'ont pas pu être imprimées car elles ne sont pas prises en charge ou sont protégées.",
"print_report_collection_content_other": "Les {{count}} notes de la collection n'ont pas pu être imprimées car elles ne sont pas prises en charge ou sont protégées."
"print_report_collection_details_ignored_notes": "Notes ignorées"
},
"note_title": {
"placeholder": "saisir le titre de la note ici...",
@@ -1601,24 +1560,17 @@
"note_type_switcher_label": "Basculer de {{type}} à :",
"note_type_switcher_others": "Autre type de note",
"note_type_switcher_templates": "Modèle",
"note_type_switcher_collection": "Collection",
"edited_notes": "Notes éditées ce jour",
"promoted_attributes": "Attributs promus"
"note_type_switcher_collection": "Collection"
},
"search_result": {
"no_notes_found": "Aucune note n'a été trouvée pour les paramètres de recherche donnés.",
"search_not_executed": "La recherche n'a pas encore été exécutée.",
"search_now": "Recherche maintenant"
"search_not_executed": "La recherche n'a pas encore été exécutée. Cliquez sur le bouton \"Rechercher\" ci-dessus pour voir les résultats."
},
"spacer": {
"configure_launchbar": "Configurer la Barre de raccourcis"
},
"sql_result": {
"no_rows": "Aucune ligne n'a été renvoyée pour cette requête",
"not_executed": "La requête n'a pas encore été exécutée.",
"failed": "L'exécution de requêtes SQL a échoué",
"statement_result": "Résultat de la déclaration",
"execute_now": "Exécuter maintenant"
"no_rows": "Aucune ligne n'a été renvoyée pour cette requête"
},
"sql_table_schemas": {
"tables": "Tableaux"
@@ -1741,7 +1693,7 @@
"paste": "Coller",
"paste-as-plain-text": "Coller comme texte brut",
"search_online": "Rechercher «{{term}}» avec {{searchEngine}}",
"search_in_trilium": "Rechercher « {{term}} » dans Trilium"
"search_in_trilium": "Rechercher \"{{term}}\" dans Trilium"
},
"image_context_menu": {
"copy_reference_to_clipboard": "Copier la référence dans le presse-papiers",
@@ -1751,15 +1703,14 @@
"open_note_in_new_tab": "Ouvrir la note dans un nouvel onglet",
"open_note_in_new_split": "Ouvrir la note dans une nouvelle division",
"open_note_in_new_window": "Ouvrir la note dans une nouvelle fenêtre",
"open_note_in_popup": "Édition rapide",
"open_note_in_other_split": "Ouvrir la note dans l'autre volet"
"open_note_in_popup": "Édition rapide"
},
"electron_integration": {
"desktop-application": "Application de bureau",
"native-title-bar": "Barre de titre native",
"native-title-bar-description": "Sous Windows et macOS, désactiver la barre de titre native rend l'application plus compacte. Sous Linux, le maintien de la barre de titre native permet une meilleure intégration avec le reste du système.",
"background-effects": "Activer les effets d'arrière-plan",
"background-effects-description": "Ajoute un arrière-plan flou et élégant aux fenêtres d'application, créant de la profondeur et un style moderne. La « barre de titre native » doit être désactivée.",
"background-effects": "Activer les effets d'arrière-plan (Windows 11 uniquement)",
"background-effects-description": "L'effet Mica ajoute un fond flou et élégant aux fenêtres de l'application, créant une profondeur et un style moderne.",
"restart-app-button": "Redémarrez l'application pour afficher les modifications",
"zoom-factor": "Facteur de zoom"
},
@@ -1778,8 +1729,7 @@
"geo-map": {
"create-child-note-title": "Créer une nouvelle note enfant et l'ajouter à la carte",
"create-child-note-instruction": "Cliquez sur la carte pour créer une nouvelle note à cet endroit ou appuyez sur Échap pour la supprimer.",
"unable-to-load-map": "Impossible de charger la carte.",
"create-child-note-text": "Ajouter le marqueur"
"unable-to-load-map": "Impossible de charger la carte."
},
"geo-map-context": {
"open-location": "Ouvrir la position",
@@ -1884,13 +1834,12 @@
"book_properties_config": {
"hide-weekends": "Masquer les week-ends",
"display-week-numbers": "Afficher les numéros de semaine",
"map-style": "Style de carte",
"map-style": "Style de carte :",
"max-nesting-depth": "Profondeur d'imbrication maximale :",
"raster": "Trame",
"vector_light": "Vecteur (clair)",
"vector_dark": "Vecteur (foncé)",
"show-scale": "Afficher l'échelle",
"show-labels": "Afficher les noms des marqueurs"
"show-scale": "Afficher l'échelle"
},
"table_context_menu": {
"delete_row": "Supprimer la ligne"
@@ -1911,7 +1860,7 @@
"add-column-placeholder": "Entrez le nom de la colonne...",
"edit-note-title": "Cliquez pour modifier le titre de la note",
"edit-column-title": "Cliquez pour modifier le titre de la colonne",
"column-already-exists": "Cette colonne existe déjà sur le tableau."
"column-already-exists": "Cette colonne existe déjà dans le tableau."
},
"presentation_view": {
"edit-slide": "Modifier cette diapositive",
@@ -1941,30 +1890,22 @@
"next_theme_message": "Vous utilisez actuellement le thème hérité de l'ancienne version, souhaitez-vous essayer le nouveau thème?",
"next_theme_button": "Essayez le nouveau thème",
"background_effects_title": "Les effets d'arrière-plan sont désormais stables",
"background_effects_message": "Sur les appareils Windows et macOS les effets d'arrière-plan sont désormais stables. Ils ajoutent une touche de couleur à l'interface utilisateur en floutant l'arrière-plan.",
"background_effects_message": "Sur les appareils Windows, les effets d'arrière-plan sont désormais parfaitement stables. Ils ajoutent une touche de couleur à l'interface utilisateur en floutant l'arrière-plan. Cette technique est également utilisée dans d'autres applications comme l'Explorateur Windows.",
"background_effects_button": "Activer les effets d'arrière-plan",
"dismiss": "Rejeter",
"new_layout_title": "Nouvelle mise en page",
"new_layout_message": "Nous avons introduit une mise en page modernisée pour Trilium. Le ruban a été supprimé et intégré de manière transparente dans l'interface principale, avec une nouvelle barre d'état et des sections extensibles (telles que les attributs promus) reprenant les fonctions clés.\n\nLa nouvelle mise en page est activée par défaut et peut être temporairement désactivée via Options → Apparence.",
"new_layout_button": "Plus d'infos"
"dismiss": "Rejeter"
},
"settings": {
"related_settings": "Paramètres associés"
},
"settings_appearance": {
"related_code_blocks": "Schéma de coloration syntaxique pour les blocs de code dans les notes de texte",
"related_code_notes": "Schéma de couleurs pour les notes de code",
"ui": "Interface utilisateur",
"ui_old_layout": "Ancienne mise en page",
"ui_new_layout": "Nouvelle mise en page"
"related_code_notes": "Schéma de couleurs pour les notes de code"
},
"units": {
"percentage": "%"
},
"pagination": {
"total_notes": "{{count}} notes",
"prev_page": "Page précédente",
"next_page": "Page suivante"
"total_notes": "{{count}} notes"
},
"collections": {
"rendering_error": "Impossible d'afficher le contenu en raison d'une erreur."
@@ -1983,9 +1924,8 @@
"unknown_widget": "Widget inconnu pour « {{id}} »."
},
"note_language": {
"not_set": "Langage non défini",
"configure-languages": "Configurer les langues...",
"help-on-languages": "Aide sur les langues de contenu..."
"not_set": "Non défini",
"configure-languages": "Configurer les langues..."
},
"content_language": {
"title": "Contenu des langues",
@@ -2033,288 +1973,14 @@
"title": "Options expérimentales",
"disclaimer": "Ces options sont expérimentales et peuvent provoquer une instabilité. Utilisez avec prudence.",
"new_layout_name": "Nouvelle mise en page",
"new_layout_description": "Essayez la nouvelle mise en page pour un look plus moderne et un usage améliorée. Sous réserve de changements importants dans les prochaines versions.",
"llm_name": "AI / LLM Chat",
"llm_description": "Activer la barre de chat AI et les notes de chat LLM alimentées par de grands modèles de langage."
"new_layout_description": "Essayez la nouvelle mise en page pour un look plus moderne et un usage améliorée. Sous réserve de changements importants dans les prochaines versions."
},
"read-only-info": {
"read-only-note": "Vous consultez actuellement une note en lecture seule.",
"auto-read-only-note": "Cette note s'affiche en mode lecture seule pour un chargement plus rapide.",
"edit-note": "Modifier la note"
"edit-note": "Editer la note"
},
"calendar_view": {
"delete_note": "Supprimer la note..."
},
"media": {
"play": "Lire (Espace)",
"pause": "Pause (Espace)",
"back-10s": "Retour arrière 10s (flèche gauche)",
"forward-30s": "Avance 30s",
"mute": "Silence (M)",
"unmute": "Réactiver le son (M)",
"playback-speed": "Vitesse de lecture",
"loop": "Boucle",
"disable-loop": "Désactiver la boucle",
"rotate": "Rotation",
"picture-in-picture": "Image dans l'image",
"exit-picture-in-picture": "Sortir de Image dans l'image",
"fullscreen": "Plein-écran (F)",
"exit-fullscreen": "Sortir du mode plein-écran",
"unsupported-format": "L'aperçu multimédia n'est pas disponible pour ce format de fichier:\n{{mime}}",
"zoom-to-fit": "Zoom pour remplir",
"zoom-reset": "Annuler zoom pour remplir"
},
"render": {
"setup_title": "Afficher du HTML personnalisé ou Preact JSX dans cette note",
"setup_create_sample_preact": "Créer un exemple de note avec Preact",
"setup_create_sample_html": "Créer un exemple de note avec HTML",
"setup_sample_created": "Un exemple de note a été créé en tant que note enfant.",
"disabled_description": "Ces notes de rendu proviennent d'une source externe. Pour vous protéger de contenu malveillant, elle n'est pas activée par défaut. Assurez-vous de faire confiance à la source avant de lactiver.",
"disabled_button_enable": "Activer la note de rendu"
},
"web_view_setup": {
"title": "Créez la vue de la page Web directement dans Trilium",
"url_placeholder": "Entrez ou collez l'adresse du site Web, par exemple https://triliumnotes.org",
"create_button": "Créer une vue Web",
"invalid_url_title": "Adresse invalide",
"invalid_url_message": "Insérer une adresse Web valide, par exemple https://triliumnotes.org.",
"disabled_description": "Cette vue Web a été importée à partir d'une source externe. Pour vous protéger du phishing ou du contenu malveillant, elle ne se charge pas automatiquement. Vous pouvez l'activer si vous faites confiance à la source.",
"disabled_button_enable": "Activer la vue Web"
},
"llm_chat": {
"placeholder": "Tapez un message...",
"send": "Envoyer",
"sending": "Envoi...",
"empty_state": "Démarrez une conversation en tapant un message ci-dessous.",
"searching_web": "Recherche sur le Web...",
"web_search": "Recherche sur le Web",
"note_tools": "Accès aux notes",
"sources": "Sources",
"extended_thinking": "Réflexion étendue",
"legacy_models": "Modèles hérités",
"thinking": "Réflexion...",
"thought_process": "Processus de réflexion",
"tool_calls": "{{count}} appel(s) d'outil",
"input": "Entrée",
"result": "Résultat",
"error": "Erreur",
"tool_error": "échoué",
"total_tokens": "{{total}} jetons",
"tokens_detail": "{{prompt}} prompt + {{completion}} achèvement",
"tokens_used": "{{prompt}} prompt + {{completion}} achèvement = {{total}} jetons",
"tokens_used_with_cost": "{{prompt}} prompt + {{completion}} achèvement = {{total}} jetons (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} prompt + {{completion}} achèvement = {{total}} jetons",
"tokens_used_with_model_and_cost": "{{model}}: {{prompt}} prompt + {{completion}} achèvement = {{total}} jetons (~${{cost}})",
"tokens": "jetons",
"context_used": "{{percentage}}% utilisé",
"note_context_enabled": "Cliquez pour désactiver le contexte de la note : {{title}}",
"note_context_disabled": "Cliquez pour inclure la note actuelle dans le contexte",
"no_provider_message": "Aucun fournisseur d'IA configuré. Ajoutez en un pour commencer à discuter.",
"add_provider": "Ajouter un fournisseur d'IA"
},
"sidebar_chat": {
"title": "discussion IA",
"launcher_title": "Ouvrir la discussion IA",
"new_chat": "Démarrer une nouvelle discussion",
"save_chat": "Enregistrer la discussion dans les notes",
"empty_state": "Démarrer une conversation",
"history": "Historique des discussions",
"recent_chats": "Discussions récentes",
"no_chats": "Pas de discussions précédentes"
},
"note-color": {
"clear-color": "Retirer la couleur de la note",
"set-color": "Définir la couleur de la note",
"set-custom-color": "Définir la couleur personnalisée de la note"
},
"popup-editor": {
"maximize": "Basculer sur l'éditeur complet"
},
"server": {
"unknown_http_error_title": "Erreur de communication avec le serveur",
"unknown_http_error_content": "Code de statut: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}",
"traefik_blocks_requests": "Si vous utilisez le reverse proxy Traefik, celui-ci a introduit un changement de rupture qui affecte la communication avec le serveur."
},
"tab_history_navigation_buttons": {
"go-back": "Revenir à la note précédente",
"go-forward": "Aller vers la note suivante"
},
"breadcrumb": {
"hoisted_badge": "Remonté",
"hoisted_badge_title": "Redescendu",
"workspace_badge": "Espace de travail",
"scroll_to_top_title": "Aller au début de la note",
"create_new_note": "Créer une nouvelle note enfant",
"empty_hide_archived_notes": "Cacher les notes archivées"
},
"breadcrumb_badges": {
"read_only_explicit": "Lecture seule",
"read_only_explicit_description": "Cette note a été paramétrée manuellement en lecture seule.\nCliquer pour temporairement l'éditer.",
"read_only_auto": "Lecture seule automatique",
"read_only_auto_description": "Cette note a été réglée automatiquement en mode lecture seule pour des raisons de performances. Cette limite automatique est réglable à partir des paramètres.\n\nCliquez pour la modifier temporairement.",
"read_only_temporarily_disabled": "Temporairement modifiable",
"read_only_temporarily_disabled_description": "Cette note est actuellement modifiable, mais elle est normalement en lecture seule. La note redeviendra en lecture seule dès que vous accéderez à une autre note.\n\nCliquez pour réactiver le mode lecture seule.",
"shared_publicly": "Partagés publiquement",
"shared_locally": "Partagé localement",
"shared_copy_to_clipboard": "Copier le lien vers le presse-papier",
"shared_open_in_browser": "Ouvrir le lien dans le navigateur",
"shared_unshare": "Supprimer le partage",
"clipped_note": "Clip Web",
"clipped_note_description": "Cette note a été initialement construite depuis l'url {{url}}.\n\nCliquez pour accéder à la page Web source.",
"execute_script": "Exécuter le script",
"execute_script_description": "Cette note est une note de script. Cliquez pour exécuter le script.",
"execute_sql": "Exécuter la commande SQL",
"execute_sql_description": "Cette note est une note SQL. Cliquer pour exécuter la requête SQL.",
"save_status_saved": "Enregister",
"save_status_saving": "Enregistrement...",
"save_status_unsaved": "Non sauvée",
"save_status_error": "La sauvegarde a échoué",
"save_status_saving_tooltip": "Les modifications sont enregistrées.",
"save_status_unsaved_tooltip": "Il y a des changements non enregistrés. Ils seront enregistrés automatiquement dans un instant.",
"save_status_error_tooltip": "Une erreur s'est produite lors de l'enregistrement de la note. Si possible, essayez de copier le contenu de la note ailleurs et de recharger l'application."
},
"right_pane": {
"toggle": "Basculer le panneau de droite",
"custom_widget_go_to_source": "Aller sur le code source",
"empty_message": "Rien à afficher pour cette note",
"empty_button": "Cacher le panneau"
},
"pdf": {
"attachments_one": "{{count}} pièce jointe",
"attachments_many": "{{count}} pièces jointes",
"attachments_other": "{{count}} pièces jointes",
"layers_one": "{{count}} couche",
"layers_many": "{{count}} couches",
"layers_other": "{{count}} couches",
"pages_one": "{{count}} page",
"pages_many": "{{count}} pages",
"pages_other": "{{count}} pages",
"pages_alt": "Page {{pageNumber}}",
"pages_loading": "Chargement..."
},
"platform_indicator": {
"available_on": "Disponible sur {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} onglet",
"title_many": "{{count}} onglets",
"title_other": "{{count}} onglets",
"more_options": "Autres options"
},
"bookmark_buttons": {
"bookmarks": "Signets"
},
"active_content_badges": {
"type_icon_pack": "pack d'icônes",
"type_backend_script": "Script backend",
"type_frontend_script": "Script frontend",
"type_widget": "Widget",
"type_app_css": "CSS personnalisé",
"type_render_note": "Note de rendu",
"type_web_view": "Vue Web",
"type_app_theme": "Thème personnalisé",
"toggle_tooltip_enable_tooltip": "Cliquer pour activer {{type}}.",
"toggle_tooltip_disable_tooltip": "Cliquer pour désactiver ce {{type}}.",
"menu_docs": "Ouvrir la documentation",
"menu_execute_now": "Exécuter le script maintenant",
"menu_run": "Démarrer automatiquement",
"menu_run_disabled": "Manuellement",
"menu_run_backend_startup": "Lorsque le backend commence",
"menu_run_hourly": "Horaire",
"menu_run_daily": "Quotidien",
"menu_run_frontend_startup": "Lorsque le frontend du bureau démarre",
"menu_run_mobile_startup": "Lorsque le frontend mobile démarre",
"menu_change_to_widget": "Passer au widget",
"menu_change_to_frontend_script": "Passer au script frontend",
"menu_theme_base": "Thème de base"
},
"setup_form": {
"more_info": "En savoir plus"
},
"mermaid": {
"placeholder": "Tapez le contenu de votre diagramme Mermaid ou utilisez l'un des diagrammes de l'échantillon ci-dessous.",
"sample_diagrams": "Diagrammes d 'exemple:",
"sample_flowchart": "Organigramme",
"sample_class": "Classe",
"sample_sequence": "Séquence",
"sample_entity_relationship": "Entité relationnelle",
"sample_state": "État",
"sample_mindmap": "Carte mentale",
"sample_architecture": "Architecture",
"sample_block": "Bloc",
"sample_c4": "C4",
"sample_gantt": "Gantt",
"sample_git": "Git",
"sample_kanban": "Kanban",
"sample_packet": "Paquet",
"sample_pie": "Camembert",
"sample_quadrant": "Quadrant",
"sample_radar": "Radar",
"sample_requirement": "Exigence",
"sample_sankey": "Sankey",
"sample_timeline": "Chronologie",
"sample_treemap": "Arborescence",
"sample_user_journey": "Utilisateur Journey",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
},
"mind-map": {
"addChild": "Ajouter un enfant",
"addParent": "Ajouter parent",
"addSibling": "Ajouter un frère",
"removeNode": "Supprimer le nœud",
"focus": "Mode Focus",
"cancelFocus": "Annuler le mode Focus",
"moveUp": "Monter",
"moveDown": "Descendre",
"link": "Lien",
"linkBidirectional": "Lien bidirectionnel",
"clickTips": "Cliquer sur le nœud cible",
"summary": "Résumé"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "Configurer les intégrations AI et les LLM (Large Language Model).",
"add_provider": "Ajouter le fournisseur",
"add_provider_title": "Ajouter le fournisseur d'IA",
"configured_providers": "Fournisseurs configurés",
"no_providers_configured": "Aucun fournisseur n'est encore configuré.",
"provider_name": "Nom",
"provider_type": "Fournisseur",
"actions": "Actions",
"delete_provider": "Supprimer",
"delete_provider_confirmation": "Êtes-vous sûr de vouloir supprimer le fournisseur \"{{name}}\"?",
"api_key": "Clé API",
"api_key_placeholder": "Entrer votre clé API",
"cancel": "Annuler"
},
"status_bar": {
"language_title": "Changer de langue",
"note_info_title": "Afficher les informations sur les notes (par exemple, dates, taille des notes)",
"backlinks_one": "{{count}} rétrolien",
"backlinks_many": "{{count}} rétroliens",
"backlinks_other": "{{count}} rétroliens",
"backlinks_title_one": "voir le rétrolien",
"backlinks_title_many": "voir les rétroliens",
"backlinks_title_other": "voir les rétroliens",
"attachments_one": "{{count}} pièce-jointe",
"attachments_many": "{{count}} pièces-jointes",
"attachments_other": "{{count}} pièces-jointes",
"attachments_title_one": "Voir la pièce-jointe dans un nouvel onglet",
"attachments_title_many": "Voir les pièces-jointes dans un nouvel onglet",
"attachments_title_other": "Voir les pièces-jointes dans un nouvel onglet",
"attributes_one": "{{count}} attribut",
"attributes_many": "{{count}} attributs",
"attributes_other": "{{count}} attributs",
"attributes_title": "Attributs propres et attributs hérités",
"note_paths_one": "{{count}} chemin",
"note_paths_many": "{{count}} chemins",
"note_paths_other": "{{count}} chemins",
"note_paths_title": "Chemins de la note",
"code_note_switcher": "Changer de langue"
},
"attributes_panel": {
"title": "Attributs de la note"
"delete_note": "Effacer la note..."
}
}

View File

@@ -399,7 +399,7 @@
"calendar_root": "nóta marcáilte ar cheart a úsáid mar fhréamh do nótaí lae. Níor cheart ach ceann amháin a mharcáil mar sin.",
"archived": "Ní bheidh nótaí leis an lipéad seo le feiceáil de réir réamhshocraithe i dtorthaí cuardaigh (i ndialóga Léim Chuig, Cuir Nasc Leis srl. chomh maith).",
"exclude_from_export": "ní chuirfear nótaí (lena bhfo-chrann) san áireamh in aon onnmhairiú nótaí",
"run": "Sainmhíníonn sé seo cé na himeachtaí ar cheart don script rith orthu. Is iad seo a leanas na luachanna féideartha:\n<ul>\n<li>frontendStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ach ní ar fhóin phóca.</li>\n<li>mobileStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ar fhóin phóca.</li>\n<li>backendStartup - nuair a thosaíonn cúltaca Trilium.</li>\n<li>hourly - rith uair san uair. Is féidir leat lipéad breise <code>runAtHour</code> a úsáid chun a shonrú cén uair a ritheann sé.</li>\n<li>daily - rith uair sa lá.</li>\n</ul>",
"run": "Sainmhíníonn sé seo cé na himeachtaí ar cheart don script rith orthu. Is iad seo a leanas na luachanna féideartha:\n<ul>\n<li>frontendStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ach ní ar fhóin phóca.</li>\n<li>mobileStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ar fhóin phóca.</li>\n<li>backendStartup - nuair a thosaíonn cúltaca Trilium</li>\n<li>uair an chloig - rith uair san uair. Is féidir leat lipéad breise <code>runAtHour</code> a úsáid chun a shonrú cén uair a ritheann sé.</li>\n<li>daily - rith uair sa lá</li>\n</ul>",
"run_on_instance": "Sainmhínigh cén sampla de Trilium ba chóir é seo a rith air. Réamhshocrú do gach sampla.",
"run_at_hour": "Cén uair ar cheart é seo a rith? Ba cheart é a úsáid i dteannta <code>#run=hourly</code>. Is féidir é seo a shainiú arís agus arís eile le haghaidh níos mó ritheanna i rith an lae.",
"disable_inclusion": "Ní chuirfear scripteanna leis an lipéad seo san áireamh i bhforghníomhú an scripte tuismitheora.",
@@ -709,8 +709,7 @@
"export_as_image": "Easpórtáil mar íomhá",
"export_as_image_png": "PNG (rastar)",
"export_as_image_svg": "SVG (veicteoir)",
"note_map": "Léarscáil nótaí",
"view_ocr_text": "Féach ar théacs OCR"
"note_map": "Léarscáil nótaí"
},
"onclick_button": {
"no_click_handler": "Níl aon láimhseálaí cliceáil sainithe ag an ngiuirléid cnaipe '{{componentId}}'"
@@ -1071,8 +1070,7 @@
"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",
"rename_relation": "Athainmnigh an gaol"
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur"
},
"backend_log": {
"refresh": "Athnuachan"
@@ -1129,9 +1127,7 @@
"title": "Roghanna Turgnamhacha",
"disclaimer": "Is roghanna turgnamhacha iad seo agus dfhéadfadh éagobhsaíocht a bheith mar thoradh orthu. Bain úsáid astu go cúramach.",
"new_layout_name": "Leagan Amach Nua",
"new_layout_description": "Bain triail as an leagan amach nua le haghaidh cuma níos nua-aimseartha agus inúsáidteachta feabhsaithe. Tá sé faoi réir athruithe móra sna heisiúintí atá le teacht.",
"llm_name": "Comhrá AI / LLM",
"llm_description": "Cumasaigh an taobhbharra comhrá AI agus nótaí comhrá LLM faoi thiomáint ag samhlacha teanga móra."
"new_layout_description": "Bain triail as an leagan amach nua le haghaidh cuma níos nua-aimseartha agus inúsáidteachta feabhsaithe. Tá sé faoi réir athruithe móra sna heisiúintí atá le teacht."
},
"fonts": {
"theme_defined": "Téama sainmhínithe",
@@ -1226,28 +1222,12 @@
},
"images": {
"images_section_title": "Íomhánna",
"download_images_automatically": "Íomhánna a íoslódáil go huathoibríoch",
"download_images_description": "Íoslódáil íomhánna tagartha ar líne ó HTML greamaithe ionas go mbeidh siad ar fáil as líne.",
"enable_image_compression": "Comhbhrú íomhá",
"max_image_dimensions": "Uasmhéid toisí íomhá",
"download_images_automatically": "Íoslódáil íomhánna go huathoibríoch le húsáid as líne.",
"download_images_description": "Is féidir tagairtí díomhánna ar líne a bheith i HTML greamaithe, aimseoidh Trilium na tagairtí sin agus íoslódálfaidh sé na híomhánna ionas go mbeidh siad ar fáil as líne.",
"enable_image_compression": "Cumasaigh comhbhrú íomhá",
"max_image_dimensions": "Uasleithead/airde íomhá (athrófar méid na híomhá má sháraíonn sí an socrú seo).",
"max_image_dimensions_unit": "picteilíní",
"jpeg_quality_description": "Is é an raon molta ná 5085. Laghdaíonn luachanna níos ísle méid an chomhaid, agus coinníonn luachanna níos airde sonraí.",
"enable_image_compression_description": "Comhbhrúigh agus athraigh méid íomhánna nuair a uaslódálfar nó a ghreamaítear iad.",
"max_image_dimensions_description": "Athrófar méid íomhánna a sháraíonn an méid seo go huathoibríoch.",
"jpeg_quality": "Cáilíocht JPEG",
"ocr_section_title": "Eastóscadh Téacs (OCR)",
"ocr_related_content_languages": "Teangacha ábhair (a úsáidtear le haghaidh eastóscadh téacs)",
"ocr_auto_process": "Próiseáil comhaid nua go huathoibríoch",
"ocr_auto_process_description": "Bain téacs go huathoibríoch as comhaid atá uaslódáilte nó greamaithe le déanaí.",
"ocr_min_confidence": "Íosmhuinín",
"ocr_confidence_description": "Ná bain ach téacs os cionn an tairsí muiníne seo. Cuimsíonn luachanna níos ísle níos mó téacs ach d'fhéadfadh siad a bheith níos lú cruinn.",
"batch_ocr_title": "Próiseáil Comhaid atá ann cheana",
"batch_ocr_description": "Bain téacs as na híomhánna, na PDFanna agus na doiciméid Office go léir atá i do nótaí. Dfhéadfadh sé seo roinnt ama a thógáil ag brath ar líon na gcomhad.",
"batch_ocr_start": "Tosaigh Próiseáil Bhaisc",
"batch_ocr_starting": "Ag tosú próiseáil bhaisc...",
"batch_ocr_progress": "Ag próiseáil {{processed}} de {{total}} comhad...",
"batch_ocr_completed": "Próiseáil bhaisc críochnaithe! Próiseáladh {{processed}} comhad.",
"batch_ocr_error": "Earráid le linn próiseála baisce: {{error}}"
"jpeg_quality_description": "Cáilíocht JPEG (10 - an caighdeán is measa, 100 - an caighdeán is fearr, moltar 50 - 85)"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Am Teorann Scriosadh Ceangaltáin",
@@ -1293,7 +1273,7 @@
"custom_name_label": "Ainm innill chuardaigh saincheaptha",
"custom_name_placeholder": "Saincheap ainm an innill chuardaigh",
"custom_url_label": "Ba chóir go mbeadh {keyword} san áireamh mar áitchoinneálaí don téarma cuardaigh i URL inneall cuardaigh saincheaptha.",
"custom_url_placeholder": "Saincheap URL an innill chuardaigh",
"custom_url_placeholder": "Saincheap url an innill chuardaigh",
"save_button": "Sábháil"
},
"tray": {
@@ -1469,15 +1449,12 @@
"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": "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."
"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."
},
"sync_2": {
"config_title": "Cumraíocht Sioncrónaithe",
@@ -1595,8 +1572,7 @@
"task-list": "Liosta Tascanna",
"new-feature": "Nua",
"collections": "Bailiúcháin",
"spreadsheet": "Scarbhileog",
"llm-chat": "Comhrá AI"
"spreadsheet": "Scarbhileog"
},
"protect_note": {
"toggle-on": "Cosain an nóta",
@@ -1924,7 +1900,7 @@
},
"content_language": {
"title": "Teangacha ábhair",
"description": "Roghnaigh teanga amháin nó níos mó ar cheart dóibh a bheith le feiceáil sa rogha teanga sa rannán Airíonna Bunúsacha de nóta téacs inléite amháin nó in-eagarthóireachta. Ceadaíonn sé seo gnéithe ar nós seiceáil litrithe, tacaíocht ó dheis go clé agus eastóscadh téacs (OCR)."
"description": "Roghnaigh teanga amháin nó níos mó ar cheart dóibh a bheith le feiceáil sa rogha teanga sa rannán Airíonna Bunúsacha de nóta téacs inléite amháin nó in-eagarthóireachta. Ceadóidh sé seo gnéithe ar nós seiceáil litrithe tacaíocht ó dheis go clé."
},
"switch_layout_button": {
"title_vertical": "Bog an painéal eagarthóireachta go dtí an bun",
@@ -2298,121 +2274,6 @@
"sample_user_journey": "Turas Úsáideora",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa",
"sample_treeview": "Radharc Crann",
"sample_wardley": "Léarscáil Wardley"
},
"llm_chat": {
"placeholder": "Clóscríobh teachtaireacht...",
"send": "Seol",
"sending": "Ag seoladh...",
"empty_state": "Tosaigh comhrá trí theachtaireacht a chlóscríobh thíos.",
"searching_web": "Ag cuardach an ghréasáin...",
"web_search": "Cuardach gréasáin",
"note_tools": "Rochtain nótaí",
"sources": "Foinsí",
"extended_thinking": "Smaointeoireacht leathnaithe",
"legacy_models": "Samhlacha oidhreachta",
"thinking": "Ag smaoineamh...",
"thought_process": "Próiseas smaointeoireachta",
"tool_calls": "{{count}} glao(í) uirlisí",
"input": "Ionchur",
"result": "Toradh",
"error": "Earráid",
"tool_error": "theip",
"total_tokens": "{{total}} comharthaí",
"tokens_detail": "leid {{prompt}} + críochnú {{completion}}",
"tokens_used": "{{prompt}} leid + {{completion}} críochnú = {{total}} comharthaí",
"tokens_used_with_cost": "{{prompt}} leid + {{completion}} críochnú = {{total}} comharthaí (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} leid + {{completion}} críochnú = {{total}} comharthaí",
"tokens_used_with_model_and_cost": "{{model}}: leid {{prompt}} + críochnú {{completion}} = {{total}} comharthaí (~${{cost}})",
"tokens": "comharthaí",
"context_used": "Úsáideadh {{percentage}}%",
"note_context_enabled": "Cliceáil chun comhthéacs nótaí a dhíchumasú: {{title}}",
"note_context_disabled": "Cliceáil chun an nóta reatha a chur san áireamh i gcomhthéacs",
"no_provider_message": "Níl aon soláthraí AI cumraithe. Cuir ceann leis chun comhrá a thosú.",
"add_provider": "Cuir Soláthraí AI leis",
"sources_summary": "{{count}} foinsí ó {{sites}} suíomhanna"
},
"sidebar_chat": {
"title": "Comhrá AI",
"launcher_title": "Oscail Comhrá AI",
"new_chat": "Tosaigh comhrá nua",
"save_chat": "Sábháil comhrá sna nótaí",
"empty_state": "Tosaigh comhrá",
"history": "Stair chomhrá",
"recent_chats": "Comhráite le déanaí",
"no_chats": "Gan aon chomhráite roimhe seo"
},
"mind-map": {
"addChild": "Cuir páiste leis",
"addParent": "Cuir tuismitheoir leis",
"addSibling": "Cuir deartháir nó deirfiúr leis",
"removeNode": "Bain nód",
"focus": "Mód Fócais",
"cancelFocus": "Cealaigh Mód Fócais",
"moveUp": "Bog suas",
"moveDown": "Bog síos",
"link": "Nasc",
"linkBidirectional": "Nasc Déthreoch",
"clickTips": "Cliceáil ar an nód sprice le do thoil",
"summary": "Achoimre"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "Cumraigh comhtháthú idir Intleacht Shaorga agus Múnla Teanga Mór.",
"add_provider": "Cuir Soláthraí leis",
"add_provider_title": "Cuir Soláthraí AI leis",
"configured_providers": "Soláthraithe Cumraithe",
"no_providers_configured": "Níl aon soláthraithe cumraithe fós.",
"provider_name": "Ainm",
"provider_type": "Soláthraí",
"actions": "Gníomhartha",
"delete_provider": "Scrios",
"delete_provider_confirmation": "An bhfuil tú cinnte gur mian leat an soláthraí \"{{name}}\" a scriosadh?",
"api_key": "Eochair API",
"api_key_placeholder": "Cuir isteach d'eochair API",
"cancel": "Cealaigh",
"feature_not_enabled": "Cumasaigh an ghné turgnamhach LLM i Socruithe → Ardleibhéil → Gnéithe turgnamhacha chun comhtháthú AI a úsáid.",
"mcp_title": "MCP (Prótacal Comhthéacs Múnla)",
"mcp_enabled": "Freastalaí MCP",
"mcp_enabled_description": "Nochtaigh críochphointe Prótacal Comhthéacs Múnla (MCP) ionas gur féidir le cúntóirí códaithe AI (m.sh. Claude Code, GitHub Copilot) do nótaí a léamh agus a mhodhnú. Ní féidir rochtain a fháil ar an gcríochphointe ach ó localhost.",
"mcp_endpoint_title": "URL críochphointe",
"mcp_endpoint_description": "Cuir an URL seo le cumraíocht MCP do chúntóra AI",
"tools": {
"search_notes": "Cuardaigh nótaí",
"get_note": "Faigh nóta",
"get_note_content": "Faigh ábhar nótaí",
"update_note_content": "Nuashonraigh ábhar an nóta",
"append_to_note": "Cuir leis an nóta",
"create_note": "Cruthaigh nóta",
"get_attributes": "Faigh tréithe",
"get_attribute": "Faigh tréith",
"set_attribute": "Socraigh tréith",
"delete_attribute": "Scrios tréith",
"get_child_notes": "Faigh nótaí leanaí",
"get_subtree": "Faigh fo-chrann",
"load_skill": "Luchtaigh scileanna",
"web_search": "Cuardach gréasáin",
"note_in_parent": "<Note/> i <Parent/>",
"get_attachment": "Faigh ceangaltán",
"get_attachment_content": "Léigh ábhar an cheangail"
}
},
"ocr": {
"extracted_text": "Téacs Bainte (OCR)",
"extracted_text_title": "Téacs Bainte (OCR)",
"loading_text": "Ag lódáil téacs OCR...",
"no_text_available": "Níl aon téacs OCR ar fáil",
"no_text_explanation": "Níor próiseáladh an nóta seo le haghaidh eastóscadh téacs OCR nó níor aimsíodh aon téacs.",
"failed_to_load": "Theip ar lódáil téacs OCR",
"process_now": "Próiseas OCR",
"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)",
"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"
"sample_ishikawa": "Ishikawa"
}
}

View File

@@ -1461,6 +1461,9 @@
"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

@@ -520,7 +520,7 @@
"custom_name_label": "Nome del motore di ricerca personalizzato",
"custom_name_placeholder": "Personalizza il nome del motore di ricerca",
"custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.",
"custom_url_placeholder": "Personalizza indirizzo URL del motore di ricerca"
"custom_url_placeholder": "Personalizza indirizzo url del motore di ricerca"
},
"sql_table_schemas": {
"tables": "Tabelle"
@@ -538,12 +538,12 @@
"new_tab": "Nuova scheda"
},
"toc": {
"table_of_contents": "Tabella dei Contenuti",
"table_of_contents": "Sommario",
"options": "Opzioni",
"no_headings": "Nessun titolo."
},
"table_of_contents": {
"title": "Tabella dei Contenuti",
"title": "Sommario",
"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\">\"Vai a\"</a>",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">finestra \"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 la scheda riassuntiva",
"show-cheatsheet": "Mostra il foglietto illustrativo",
"toggle-zen-mode": "Modalità Zen",
"new-version-available": "Nuovo aggiornamento disponibile",
"download-update": "Ottieni la versione {{latestVersion}}",
@@ -1149,8 +1149,7 @@
"export_as_image": "Esporta come immagine",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)",
"note_map": "Mappa",
"view_ocr_text": "Visualizza il testo OCR"
"note_map": "Mappa"
},
"onclick_button": {
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
@@ -1440,8 +1439,7 @@
"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",
"rename_relation": "Rinomina relazione"
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota"
},
"vacuum_database": {
"title": "Pulizia del database",
@@ -1543,28 +1541,12 @@
},
"images": {
"images_section_title": "Immagini",
"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",
"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).",
"max_image_dimensions_unit": "pixel",
"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}}"
"jpeg_quality_description": "Qualità JPEG (10 - qualità peggiore, 100 - qualità migliore, 50 - 85 è consigliato)"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Timeout cancellazione allegato",
@@ -1672,15 +1654,12 @@
"spellcheck": {
"title": "Controllo ortografico",
"description": "Queste opzioni sono valide solo per le versioni desktop; i browser utilizzeranno il proprio controllo ortografico nativo.",
"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."
"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."
},
"api_log": {
"close": "Vicino"
@@ -1739,8 +1718,7 @@
"new-feature": "Nuovo",
"collections": "Collezioni",
"ai-chat": "Chat con IA",
"spreadsheet": "Foglio di calcolo",
"llm-chat": "Chat con IA"
"spreadsheet": "Foglio di calcolo"
},
"protect_note": {
"toggle-on": "Proteggi la nota",
@@ -1961,7 +1939,7 @@
},
"content_language": {
"title": "Lingue dei contenuti",
"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)."
"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."
},
"switch_layout_button": {
"title_vertical": "Sposta il riquadro di modifica in basso",
@@ -2073,9 +2051,7 @@
"title": "Opzioni sperimentali",
"disclaimer": "Queste opzioni sono sperimentali e potrebbero causare instabilità. Usare con cautela.",
"new_layout_name": "Nuovo layout",
"new_layout_description": "Prova il nuovo layout per un look più moderno e una maggiore usabilità. Soggetto a modifiche significative nelle prossime versioni.",
"llm_name": "Chat con IA / LLM",
"llm_description": "Attiva la barra laterale della chat con IA e le note della chat LLM basate su modelli linguistici di grandi dimensioni."
"new_layout_description": "Prova il nuovo layout per un look più moderno e una maggiore usabilità. Soggetto a modifiche significative nelle prossime versioni."
},
"server": {
"unknown_http_error_title": "Errore di comunicazione con il server",
@@ -2268,121 +2244,6 @@
"sample_user_journey": "Percorso dell'utente",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa",
"sample_treeview": "TreeView",
"sample_wardley": "Mappa di Wardley"
},
"llm_chat": {
"placeholder": "Scrivi un messaggio...",
"send": "Invia",
"sending": "Invio in corso...",
"empty_state": "Inizia una conversazione scrivendo un messaggio qui sotto.",
"searching_web": "Ricerca sul web...",
"web_search": "Ricerca sul web",
"note_tools": "Nota di accesso",
"sources": "Fonti",
"extended_thinking": "Riflessioni approfondite",
"legacy_models": "Modelli precedenti",
"thinking": "Sto riflettendo...",
"thought_process": "Processo mentale",
"tool_calls": "{{count}} chiamata/e di funzione",
"input": "Dati in ingresso",
"result": "Risultato",
"error": "Errore",
"tool_error": "fallito",
"total_tokens": "{{total}} gettoni",
"tokens_detail": "{{prompt}} prompt + {{completion}} completamento",
"tokens_used": "{{prompt}} prompt + {{completion}} completamento = {{total}} token",
"tokens_used_with_cost": "{{prompt}} prompt + {{completion}} completamento = {{total}} token (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} prompt + {{completion}} completamento = {{total}} token",
"tokens_used_with_model_and_cost": "{{model}}: {{prompt}} prompt + {{completion}} completamento = {{total}} token (~${{cost}})",
"tokens": "tokens",
"context_used": "{{percentage}}% utilizzato",
"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",
"sources_summary": "{{count}} fonti provenienti da {{sites}} siti"
},
"sidebar_chat": {
"title": "Chat AI",
"launcher_title": "Apri Chat AI",
"new_chat": "Inizia una nuova chat",
"save_chat": "Salva la chat negli appunti",
"empty_state": "Avvia una conversazione",
"history": "Cronologia delle chat",
"recent_chats": "Conversazioni recenti",
"no_chats": "Nessuna conversazione precedente"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "Configurare le integrazioni con l'intelligenza artificiale e i modelli linguistici di grandi dimensioni.",
"add_provider": "Aggiungi fornitore",
"add_provider_title": "Aggiungi un fornitore di IA",
"configured_providers": "Fornitori configurati",
"no_providers_configured": "Non sono stati ancora configurati fornitori.",
"provider_name": "Nome",
"provider_type": "Fornitore",
"actions": "Azioni",
"delete_provider": "Elimina",
"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",
"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"
"sample_ishikawa": "Ishikawa"
}
}

View File

@@ -486,8 +486,7 @@
"advanced": "高度",
"export_as_image": "画像としてエクスポート",
"export_as_image_png": "PNG (raster)",
"export_as_image_svg": "SVG (vector)",
"view_ocr_text": "OCR テキストを表示"
"export_as_image_svg": "SVG (vector)"
},
"command_palette": {
"export_note_title": "ノートをエクスポート",
@@ -602,8 +601,7 @@
"new-feature": "New",
"collections": "コレクション",
"ai-chat": "AI チャット",
"spreadsheet": "スプレッドシート",
"llm-chat": "AI チャット"
"spreadsheet": "スプレッドシート"
},
"edited_notes": {
"no_edited_notes_found": "この日の編集されたノートはまだありません...",
@@ -899,28 +897,12 @@
},
"images": {
"images_section_title": "画像",
"download_images_automatically": "画像を自動的にダウンロードする。",
"download_images_description": "貼り付けた HTML 内の参照画像をダウンロードし、オフラインで利用できるようにす。",
"enable_image_compression": "画像の圧縮",
"max_image_dimensions": "画像の最大サイズ",
"download_images_automatically": "画像を自動的にダウンロードしてオフラインで使用可能にする。",
"download_images_description": "貼り付けられたHTMLにはオンライン画像への参照が含まれていることがありますが、Triliumはそれらの参照を見つけて画像をダウンロードし、オフラインで利用できるようにします。",
"enable_image_compression": "画像の圧縮を有効にする",
"max_image_dimensions": "画像の最大幅/高さ(この設定を超えると画像はリサイズされます)。",
"max_image_dimensions_unit": "ピクセル",
"jpeg_quality_description": "推奨範囲は5085です。値が低いほどファイルサイズが小さくなり、値が高いほどディテールが保持されます。",
"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": "{{total}} ファイルのうち {{processed}} ファイルを処理中...",
"batch_ocr_completed": "バッチ処理が完了しました!{{processed}} ファイルを処理しました。",
"batch_ocr_error": "バッチ処理中にエラーが発生しました: {{error}}"
"jpeg_quality_description": "JPEGの品質10 - 最低品質、100 - 最高品質、50 - 80を推奨"
},
"search_engine": {
"title": "検索エンジン",
@@ -933,7 +915,7 @@
"custom_name_label": "カスタム検索エンジンの名前",
"custom_name_placeholder": "カスタム検索エンジンの名前",
"custom_url_label": "カスタム検索エンジンのURLには、検索語句のプレースホルダーとして {keyword} を含める必要があります。",
"custom_url_placeholder": "検索エンジンの URL をカスタマイズ",
"custom_url_placeholder": "カスタム検索エンジンのurl",
"save_button": "保存"
},
"tray": {
@@ -1027,15 +1009,12 @@
"spellcheck": {
"title": "スペルチェック",
"description": "これらのオプションはデスクトップビルドにのみ適用され、ブラウザはそれぞれのネイティブスペルチェックを使用します。",
"enable": "スペルチェック",
"language_code_label": "スペルチェック対応言語",
"restart-required": "スペルチェックオプションの変更は、アプリケーションの再起動後に有効になります。",
"custom_dictionary_title": "カスタム辞書",
"custom_dictionary_description": "辞書に追加した単語は、すべてのデバイス間で同期されます。",
"custom_dictionary_edit": "カスタム単語",
"custom_dictionary_edit_description": "スペルチェッカーでエラーとして検出されないようにする単語リストを編集します。変更は再起動後に反映されます。",
"custom_dictionary_open": "辞書の編集",
"related_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": {
"config_title": "同期設定",
@@ -1123,7 +1102,7 @@
"calendar_root": "dayートのルートとして使用するートをマークします。このようにマークできるのは 1 つだけです。",
"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 - 1時間ごとに実行。 <code>runAtHour</code> というラベルを追加することで、実行時刻を指定できます。</li>\n<li>daily - 1日に1回実行</li>\n</ul>",
"run": "どのイベントでスクリプトを実行するを定義します。可能な値はの通り:\n<ul>\n<li>frontendStartup - Trilium フロントエンド起動(または更新)されたとき。モバイルは除く</li>\n<li>mobileStartup - モバイルで Trilium フロントエンド起動(または更新)されたとき。</li>\n<li>backendStartup - Trilium バックエンド起動したとき</li>\n<li>hourly - 1時間に1回実行します。 <code>runAtHour</code> というラベルを追加して、実行時刻を指定できます。</li>\n<li>daily - 1日に1回実行</li>\n</ul>",
"run_on_instance": "どの Trilium インスタンスでこれを実行するかを定義します。デフォルトはすべてのインスタンスです。",
"run_at_hour": "何時に実行するかを指定します。 <code>#run=hourly</code> と併用してください。1日に複数回実行したい場合は、複数回定義できます。",
"disable_inclusion": "このラベルが付いたスクリプトは親スクリプトの実行には含まれません。",
@@ -1411,7 +1390,7 @@
},
"content_language": {
"title": "コンテンツの言語",
"description": "読み取り専用または編集可能なテキストノートの基本プロパティセクションの言語選択に表示する言語を 1 つ以上選択してください。これにより、スペルチェック右から左へのサポート、テキスト抽出OCRなどの機能が利用できるようになります。"
"description": "読み取り専用または編集可能なテキストノートの基本プロパティセクションの言語選択に表示する言語を 1 つ以上選択します。これにより、スペルチェック右から左へのサポートなどの機能が利用できるようになります。"
},
"png_export_button": {
"button_title": "図をPNG形式でエクスポート"
@@ -1573,8 +1552,7 @@
"click_on_canvas_to_place_new_note": "キャンバスをクリックして新しいノートを配置",
"connection_exists": "これらのノート間の接続 '{{name}}' は既に存在します。",
"start_dragging_relations": "ここからリレーションをドラッグして、別のノートにドロップします。",
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。",
"rename_relation": "リレーション名の変更"
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。"
},
"database_anonymization": {
"title": "データベースの匿名化",
@@ -2072,9 +2050,7 @@
"title": "実験オプション",
"disclaimer": "これらのオプションは試験的なもので、動作が不安定になる可能性があります。注意してご使用ください。",
"new_layout_name": "新しいレイアウト",
"new_layout_description": "よりモダンな外観と使いやすさが向上した新しいレイアウトをお試しください。今後のリリースで大幅な変更が加えられる可能性があります。",
"llm_name": "AI / LLM チャット",
"llm_description": "大規模言語モデルを活用した AI チャットサイドバーと LLM チャットノートを有効にします。"
"new_layout_description": "よりモダンな外観と使いやすさが向上した新しいレイアウトをお試しください。今後のリリースで大幅な変更が加えられる可能性があります。"
},
"breadcrumb_badges": {
"read_only_explicit": "読み取り専用",
@@ -2238,121 +2214,6 @@
"sample_user_journey": "ユーザージャーニー図",
"sample_xy": "XY チャート",
"sample_venn": "ベン図",
"sample_ishikawa": "石川図",
"sample_treeview": "ツリービュー",
"sample_wardley": "ウォードリーマップ"
},
"llm_chat": {
"placeholder": "メッセージを入力してください…",
"send": "送信",
"sending": "送信中...",
"empty_state": "下記にメッセージを入力して会話を始めましょう。",
"searching_web": "ウェブ検索中…",
"web_search": "ウェブ検索",
"note_tools": "ノートへのアクセス",
"sources": "ソース",
"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 プロバイダーを追加",
"sources_summary": "{{count}} 件のソースを {{sites}} サイトから取得"
},
"sidebar_chat": {
"title": "AI チャット",
"launcher_title": "AI チャットを開く",
"new_chat": "新しいチャットを開始",
"save_chat": "チャットをノートに保存",
"empty_state": "会話を開始",
"history": "チャット履歴",
"recent_chats": "最近のチャット",
"no_chats": "過去のチャットはありません"
},
"mind-map": {
"addChild": "子ノードを追加",
"addParent": "親ノードを追加",
"addSibling": "兄弟ノードを追加",
"removeNode": "ノードを削除",
"focus": "フォーカスモード",
"cancelFocus": "フォーカスモードを解除",
"moveUp": "上に移動",
"moveDown": "下に移動",
"link": "リンク",
"linkBidirectional": "双方向リンク",
"clickTips": "対象ノードをクリックしてください",
"summary": "概要"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "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": "キャンセル",
"feature_not_enabled": "AI 連携機能を使用するには、「設定」→「高度」→「実験的機能」で LLM 実験的機能を有効にしてください。",
"mcp_title": "MCPモデル コンテキスト プロトコル)",
"mcp_enabled": "MCP サーバー",
"mcp_enabled_description": "AI コーディングアシスタントClaude Code、GitHub Copilotートを読み取って変更できるように、モデルコンテキストプロトコルMCPエンドポイントを公開します。このエンドポイントは localhost からのみアクセス可能です。",
"mcp_endpoint_title": "エンドポイント URL",
"mcp_endpoint_description": "この URL を 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": "Web 検索",
"note_in_parent": "<Note/> を <Parent/>",
"get_attachment": "添付ファイルを取得",
"get_attachment_content": "添付ファイルの内容を読み取る"
}
},
"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 処理が開始されました。しばらくお待ちいただき、ページを更新してください。",
"processing_failed": "OCR 処理の開始に失敗しました",
"view_extracted_text": "抽出されたテキストOCRを表示",
"processing_complete": "OCR 処理が完了しました。",
"text_filtered_low_confidence": "OCR は {{confidence}}% の信頼度でテキストを検出しましたが、最小しきい値が {{threshold}}% であるため、破棄されました。",
"open_media_settings": "設定を開く"
"sample_ishikawa": "石川図"
}
}

View File

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

View File

@@ -1665,6 +1665,9 @@
"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,6 +1435,9 @@
"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,6 +1944,9 @@
"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

@@ -875,7 +875,7 @@
"print_note": "Imprimare notiță",
"re_render_note": "Reinterpretare notiță",
"save_revision": "Salvează o nouă revizie",
"advanced": "Avansat",
"advanced": "Advansat",
"search_in_note": "Caută în notiță",
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
@@ -1237,9 +1237,12 @@
"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."
},
@@ -2134,5 +2137,55 @@
"title_few": "{{count}} taburi",
"title_other": "{{count}} de taburi",
"more_options": "Mai multe opțiuni"
},
"setup": {
"heading": "Începeți cu Trilium",
"new-document": "Nouă bază de cunoștințe",
"new-document-description": "Începeți cu o bază de cunoștințe curată și începeți imediat.",
"sync-from-desktop": "Conectează o aplicație desktop",
"sync-from-desktop-description": "Aveți doar o aplicație Trilium desktop rulând pe un alt dispozitiv. Acest dispozitiv va sincroniza datele de la acea aplicație desktop.",
"sync-from-server": "Conectează-te la un server existent",
"sync-from-server-description": "Aveți un server Trilium care rulează în altă parte (fie auto-găzduit, fie în cloud). Acest dispozitiv va sincroniza datele de la acel server.",
"next": "Următorul",
"init-in-progress": "Inițializarea documentului în curs",
"redirecting": "Veți fi redirecționat în scurt timp către aplicație.",
"title": "Configurare",
"sync-from-server-page-description": "Introduceți detaliile serverului dvs. mai jos pentru a vă conecta la spațiul de lucru existent.",
"sync-in-progress-title": "Sincronizare în curs",
"sync-in-progress-description": "Dispozitivul dvs. este acum conectat și elementele sunt sincronizate.",
"button-back": "Înapoi",
"button-finish-setup": "Finalizează configurarea",
"sync-step-connecting": "Conectare la server",
"sync-step-syncing": "Sincronizare date",
"sync-step-finalizing": "Setarea opțiunilor",
"create-new-document-options-title": "Cum doriți să începeți?",
"create-new-document-options-with-demo": "Cu conținut demonstrativ",
"create-new-document-options-with-demo-description": "Explorează Trilium cu conținut exemplu.",
"create-new-document-options-empty": "Gol",
"create-new-document-options-empty-description": "Începeți cu o bază de cunoștințe goală. Puteți importa notițe demo mai târziu.",
"create-new-document-title": "Pregătirea bazei de cunoștințe",
"create-new-document-description": "Acest proces va dura doar câteva momente.",
"sync-illustration-this-device": "Acest dispozitiv",
"sync-illustration-desktop-app": "Aplicație desktop",
"sync-illustration-server": "Server de sincronizare",
"sync-from-desktop-step1": "Deschideți aplicația Trilium Notes pentru desktop.",
"sync-from-desktop-step2": "Din meniul Trilium, dați clic pe Opțiuni.",
"sync-from-desktop-step3": "Clic pe categoria „Sincronizare”.",
"sync-from-desktop-step4": "Schimbați adresa server-ului către: {{- host}} și apăsați „Salvează”.",
"sync-from-desktop-step5": "Clic pe butonul „Testează sincronizarea” pentru a verifica dacă conexiunea a fost făcută cu succes.",
"sync-from-desktop-final": "După ce ați finalizat acești pași, puteți trece la pasul următor.",
"sync-from-desktop-waiting": "Așteptare pentru conexiune...",
"advanced-options": "Opțiuni avansate",
"sync-failed": "Sincronizare eșuată: {{message}}",
"server-host": "Adresa serverului Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"server-password": "Parolă",
"proxy-server": "Server proxy (opțional)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"proxy-instruction": " Dacă lăsați setarea proxy necompletată, va fi utilizat proxy-ul sistemului.",
"dismiss-error": "Închide mesajul de eroare",
"wrong-password": "Parolă greșită. Vă rugăm să încercați din nou.",
"language": "Limbă",
"continue": "Continuă"
}
}

View File

@@ -194,7 +194,7 @@
"row-insert-child": "Создать дочернюю заметку",
"row-insert-below": "Добавить строку ниже",
"row-insert-above": "Добавить строку выше",
"new-column-relation": "Отношение"
"new-column-relation": "Связь"
},
"add_label": {
"add_label": "Добавить метку",
@@ -465,13 +465,13 @@
"related_notes_title": "Другие заметки с этой меткой",
"label": "Метка",
"label_definition": "Определение метки",
"relation": "Детали отношения",
"relation": "Отношение",
"relation_definition": "Определение отношения",
"disable_versioning": "отключает автоматическое версионирование. Полезно, например, для больших, но неважных заметок, например, для больших JS-библиотек, используемых для написания скриптов",
"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": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:<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></ul>",
"run_on_instance": "Определить, на каком экземпляре Trilium это должно выполняться. По умолчанию — для всех экземпляров.",
"run_at_hour": "В какой час это должно выполняться? Следует использовать вместе с <code>#run=hourly</code>. Можно задать несколько раз для большего количества запусков в течение дня.",
"disable_inclusion": "скрипты с этой меткой не будут включены в выполнение родительского скрипта.",
@@ -495,7 +495,7 @@
"is_owned_by_note": "принадлежит заметке",
"and_more": "... и ещё {{count}}.",
"app_theme": "отмечает заметки CSS, которые являются полноценными темами Trilium и, таким образом, доступны в опциях Trilium.",
"title_template": "заголовок по умолчанию для заметок, создаваемых как дочерние элементы текущей. Значение вычисляется как строка JavaScript \n и может быть дополнено динамическим контентом с помощью внедренных переменных <code>now</code> и <code>parentNote</code>. Например:\n \n <ul>\n <li><code>Литературные произведения ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Лог для ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Подробности см. в <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">вики</a>, документации API для <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> и <a href=\"https://day.js.org/docs/en/display/format\">now</a>.",
"title_template": "Заголовок по умолчанию для заметок, создаваемых как дочерние элементы данной заметки. Значение вычисляется как строка JavaScript\n и, таким образом, может быть дополнено динамическим контентом с помощью внедренных переменных <code>now</code> и <code>parentNote</code>. Примеры:\n \n <ul>\n <li><code>Литературные произведения ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Лог для ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Подробности см. в <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">вики</a>, документации API для <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> и <a href=\"https://day.js.org/docs/en/display/format\">now</a>.",
"icon_class": "значение этой метки добавляется в виде CSS-класса к значку в дереве, что помогает визуально различать заметки в дереве. Примером может служить bx bx-home — значки берутся из boxicons. Может использоваться в шаблонах заметок.",
"share_favicon": "Заметка о фавиконе должна быть размещена на странице общего доступа. Обычно её назначают корневой папке общего доступа и делают наследуемой. Заметка о фавиконе также должна находиться в поддереве общего доступа. Рассмотрите возможность использования атрибута 'share_hidden_from_tree'.",
"inbox": "расположение папки «Входящие» по умолчанию для новых заметок — при создании заметки с помощью кнопки «Новая заметка» на боковой панели заметки будут созданы как дочерние заметки в заметке, помеченной меткой <code>#inbox</code>.",
@@ -548,8 +548,7 @@
"render_note": "заметки типа «Рендер HTML» будут отображаться с использованием кодовой заметки (HTML или скрипта), и необходимо указать с помощью этой связи, какую заметку следует отобразить",
"widget_relation": "заметка, на которую ссылается отношение будет выполнена и отображена как виджет на боковой панели",
"share_js": "JavaScript-заметка, которая будет добавлена на страницу общего доступа. JavaScript-заметка также должна находиться в общем поддереве. Рекомендуется использовать 'share_hidden_from_tree'.",
"other_notes_with_name": "Другие заметки с {{attributeType}} названием \"{{attributeName}}\"",
"textarea": "Многострочный текст"
"other_notes_with_name": "Другие заметки с {{attributeType}} названием \"{{attributeName}}\""
},
"command_palette": {
"configure_launch_bar_description": "Откройте конфигурацию панели запуска, чтобы добавить или удалить элементы.",
@@ -836,8 +835,7 @@
"task-list": "Список задач",
"confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?",
"ai-chat": "Чат с ИИ",
"spreadsheet": "Электронная таблица",
"llm-chat": "Чат с ИИ"
"spreadsheet": "Электронная таблица"
},
"tree-context-menu": {
"open-in-popup": "Быстрое редактирование",
@@ -1017,7 +1015,7 @@
"open_sql_console_history": "Открыть историю консоли SQL",
"show_shared_notes_subtree": "Поддерево общедоступных заметок",
"switch_to_mobile_version": "Перейти на мобильную версию",
"switch_to_desktop_version": "Переключиться на версию для компьютера",
"switch_to_desktop_version": "Переключиться на версию для ПК",
"new-version-available": "Доступно обновление",
"download-update": "Обновить до {{latestVersion}}",
"search_notes": "Поиск заметок"
@@ -1639,11 +1637,11 @@
"start_dragging_relations": "Начните перетягивать отношения отсюда на другую заметку."
},
"vacuum_database": {
"title": "Уменьшение размера файла базы данных",
"description": "Это приведет к перестройке базы данных, что, скорее всего, уменьшит размер её файла. Данные не будут изменены.",
"button_text": "Уменьшить размер файла базы данных",
"vacuuming_database": "Уменьшение размера файла базы данных...",
"database_vacuumed": "База данных была перестроена"
"title": "Сжатие базы данных",
"description": "Это приведет к перестройке базы данных, что, как правило, приводит к уменьшению размера файла базы данных. Данные затронуты не будут.",
"button_text": "Сжать базу данных",
"vacuuming_database": "Сжатие БД...",
"database_vacuumed": "База данных была сжата"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Сочетания клавиш Vim",
@@ -1679,7 +1677,10 @@
"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": {
@@ -1762,8 +1763,8 @@
"database_integrity_check": {
"title": "Проверка целостности базы данных",
"description": "Это позволит проверить базу данных на предмет повреждений на уровне SQLite. Это может занять некоторое время в зависимости от размера базы данных.",
"check_button": "Проверить целостность базы данных",
"checking_integrity": "Проверка целостности базы данных...",
"check_button": "Проверить целостность БД",
"checking_integrity": "Проверка целостности БД...",
"integrity_check_succeeded": "Проверка целостности прошла успешно - проблем не обнаружено.",
"integrity_check_failed": "Проверка целостности завершена с ошибками: {{results}}"
},
@@ -2114,9 +2115,7 @@
"new_layout_description": "Попробуйте новый современный и удобный дизайн. В будущих обновлениях возможны его существенные изменения.",
"new_layout_name": "Новый дизайн",
"title": "Экспериментальные параметры",
"disclaimer": "Эти параметры экспериментальные и могут повлиять на стабильность. Используйте с осторожностью.",
"llm_name": "ИИ / LLM чат",
"llm_description": "Включить боковую панель чата с ИИ и заметки, созданные на основе больших языковых моделей (LLM)."
"disclaimer": "Эти параметры экспериментальные и могут повлиять на стабильность. Используйте с осторожностью."
},
"popup-editor": {
"maximize": "Переключить на полный редактор"
@@ -2198,123 +2197,5 @@
},
"setup_form": {
"more_info": "Узнать больше"
},
"media": {
"play": "Воспроизвести (пробел)",
"pause": "Пауза (пробел)",
"back-10s": "Назад на 10 секунд (стрелка влево)",
"forward-30s": "Вперёд на 30 секунд",
"mute": "Выключить звук (M)",
"unmute": "Включить звук (M)",
"playback-speed": "Скорость проигрывания",
"loop": "Зациклить",
"disable-loop": "Отключить зацикливание",
"rotate": "Повернуть",
"picture-in-picture": "Картинка в картинке",
"exit-picture-in-picture": "Выйти из режима \"картинка в картинке\"",
"fullscreen": "Режим полного экрана (F)",
"exit-fullscreen": "Выйти из режима полного экрана",
"unsupported-format": "Предпросмотр недоступен для данного формата файла:\n{{mime}}",
"zoom-to-fit": "Заполнить путём масштабирования",
"zoom-reset": "Сбросить заполнение путём масштабирования"
},
"llm_chat": {
"placeholder": "Введите сообщение...",
"send": "Отправить",
"sending": "Отправка...",
"empty_state": "Начните общение, написав сообщение в поле ниже.",
"searching_web": "Поиск в сети...",
"web_search": "Поиск в сети",
"note_tools": "Доступ к заметке",
"sources": "Источники",
"extended_thinking": "Расширенное мышление",
"legacy_models": "Устаревшие модели",
"thinking": "Обработка...",
"thought_process": "Процесс обработки",
"tool_calls": "{{count}} вызов(а/ов) инструмента",
"input": "Ввод",
"result": "Результат",
"error": "Ошибка",
"tool_error": "ошибка",
"total_tokens": "{{total}} токен(а/ов)",
"tokens": "токены",
"context_used": "{{percentage}}% использовано",
"note_context_enabled": "Нажмите, чтобы отключить контекст заметки: {{title}}",
"note_context_disabled": "Нажмите, чтобы включить текущую заметку в контекст",
"no_provider_message": "Не выбран провайдер ИИ. Добавьте его для начала общения.",
"add_provider": "Добавить провайдера ИИ",
"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}})"
},
"sidebar_chat": {
"title": "Чат с ИИ",
"launcher_title": "Чат с Open AI",
"new_chat": "Начать новый чат",
"save_chat": "Сохранить чат в заметках",
"empty_state": "Начать общение",
"history": "История чата",
"recent_chats": "Недавние чаты",
"no_chats": "Нет предыдущих чатов"
},
"mermaid": {
"placeholder": "Введите содержимое вашей Mermaid диаграммы или используйте один из примеров ниже.",
"sample_diagrams": "Примеры диаграм:",
"sample_flowchart": "Блок-схема",
"sample_class": "Диаграмма классов",
"sample_sequence": "Диаграмма последовательностей",
"sample_entity_relationship": "Диаграмма \"Сущность — связь\"",
"sample_state": "Диаграмма состояний",
"sample_mindmap": "Ментальная карта",
"sample_architecture": "Архитектурная схема",
"sample_block": "Структурная схема",
"sample_gantt": "Диаграмма Ганта",
"sample_git": "Git",
"sample_kanban": "Канбан",
"sample_ishikawa": "Диаграмма Исикавы",
"sample_c4": "C4",
"sample_packet": "Диаграмма сетевых пакетов",
"sample_pie": "Круговая диаграмма",
"sample_quadrant": "Квадрантная диаграмма",
"sample_radar": "Радиолокационная схема",
"sample_requirement": "Диаграмма зависимостей",
"sample_sankey": "Диаграмма Сэнки",
"sample_timeline": "Временная диаграмма",
"sample_treemap": "Древовидная диаграмма",
"sample_user_journey": "Карта пользовательского пути",
"sample_xy": "XY",
"sample_venn": "Диаграмма Венна"
},
"mind-map": {
"addChild": "Добавить дочерний элемент",
"addParent": "Добавить родительский элемент",
"addSibling": "Добавить элемент на том же уровне",
"removeNode": "Удалить узел",
"focus": "Режим фокусировки",
"cancelFocus": "Отключить режим фокусировки",
"moveUp": "Передвинуть выше",
"moveDown": "Передвинуть ниже",
"link": "Связь",
"linkBidirectional": "Двусторонняя связь",
"clickTips": "Пожалуйста, нажмите на целевой узел",
"summary": "Сводка"
},
"llm": {
"settings_title": "ИИ / LLM",
"settings_description": "Настроить интеграции ИИ и больших языковых моделей.",
"add_provider": "Добавить провайдера",
"add_provider_title": "Добавить провайдера ИИ",
"configured_providers": "Настроенные провайдеры",
"no_providers_configured": "Ещё нет настроенных провайдеров.",
"provider_name": "Название",
"provider_type": "Провайдер",
"actions": "Действия",
"delete_provider": "Удалить",
"delete_provider_confirmation": "Вы уверены, что желаете удалить провайдера \"{{name}}\"?",
"api_key": "Ключ API",
"api_key_placeholder": "Введите ваш ключ API",
"cancel": "Отмена"
}
}

View File

@@ -23,33 +23,10 @@
"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_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."
"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."
},
"export": {
"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."
"close": "Kapat"
},
"import": {
"chooseImportFile": "İçe aktarım dosyası",
@@ -81,9 +58,7 @@
"widget-render-error": {
"title": "Özel React widget'ı çizilirken sorun yaşandı"
},
"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ç"
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}"
},
"add_link": {
"add_link": "Bağlantı ekle",
@@ -128,32 +103,5 @@
"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,8 +706,7 @@
"export_as_image": "匯出為圖片",
"export_as_image_png": "PNG (點陣)",
"export_as_image_svg": "SVG (向量)",
"note_map": "筆記地圖",
"view_ocr_text": "顯示 OCR 文字"
"note_map": "筆記地圖"
},
"onclick_button": {
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
@@ -1197,28 +1196,12 @@
},
"images": {
"images_section_title": "圖片",
"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}}"
"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": "像素"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "附件清理超時",
@@ -1398,6 +1381,9 @@
"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": {
@@ -1511,8 +1497,7 @@
"new-feature": "新增",
"collections": "集合",
"ai-chat": "AI 聊天",
"spreadsheet": "試算表",
"llm-chat": "AI 對話"
"spreadsheet": "試算表"
},
"protect_note": {
"toggle-on": "保護筆記",
@@ -1881,7 +1866,7 @@
},
"content_language": {
"title": "內文語言",
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左及文字擷取 (OCR) 等功能。"
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左之類的功能。"
},
"switch_layout_button": {
"title_vertical": "將編輯面板移至底部",
@@ -2061,9 +2046,7 @@
"title": "實驗性選項",
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
"new_layout_name": "新版面配置",
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。",
"llm_name": "AI / LLM 對話",
"llm_description": "啟用由大語言模型驅動的 AI 聊天側邊欄及 LLM 聊天筆記。"
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
},
"server": {
"unknown_http_error_title": "與伺服器通訊錯誤",
@@ -2246,121 +2229,6 @@
"sample_user_journey": "使用者旅程",
"sample_xy": "XY 圖表",
"sample_venn": "韋恩圖",
"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": "無先前的對話記錄"
"sample_ishikawa": "魚骨圖"
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { IconRegistry, Locale } from "@triliumnext/commons";
import { BootstrapDefinition } from "@triliumnext/commons";
import appContext, { AppContext } from "./components/app_context";
import type FNote from "./entities/fnote";
@@ -15,16 +15,14 @@ interface ElectronProcess {
platform: string;
}
interface CustomGlobals {
interface CustomGlobals extends BootstrapDefinition {
isDesktop: typeof utils.isDesktop;
isMobile: typeof utils.isMobile;
device: "mobile" | "desktop" | "print";
getComponentByEl: typeof appContext.getComponentByEl;
getHeaders: typeof server.getHeaders;
getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: () => FNote | null;
getThemeStyle: () => "auto" | "light" | "dark";
ESLINT: Library;
appContext: AppContext;
froca: Froca;
@@ -32,33 +30,7 @@ interface CustomGlobals {
SEARCH_HELP_TEXT: string;
activeDialog: JQuery<HTMLElement> | null;
componentId: string;
csrfToken: string;
baseApiUrl: string;
isProtectedSessionAvailable: boolean;
isDev: boolean;
isMainWindow: boolean;
maxEntityChangeIdAtLoad: number;
maxEntityChangeSyncIdAtLoad: number;
assetPath: string;
appPath: string;
instanceName: string;
appCssNoteIds: string[];
triliumVersion: string;
TRILIUM_SAFE_MODE: boolean;
platform?: typeof process.platform;
linter: typeof lint;
hasNativeTitleBar: boolean;
hasBackgroundEffects: boolean;
isElectron: boolean;
isRtl: boolean;
iconRegistry: IconRegistry;
theme: string;
themeBase?: "next" | "next-light" | "next-dark";
customThemeCssUrl?: string;
iconPackCss: string;
headingStyle: "plain" | "underline" | "markdown";
layoutOrientation: "vertical" | "horizontal";
currentLocale: Locale;
}
type RequireMethod = (moduleName: string) => any;

View File

@@ -336,8 +336,6 @@ export async function getExtendedWidgetType(note: FNote | null | undefined, note
if (noteContext?.viewScope?.viewMode === "source") {
resultingType = "readOnlyCode";
} else if (noteContext.viewScope?.viewMode === "ocr") {
resultingType = "readOnlyOCRText";
} else if (noteContext.viewScope?.viewMode === "attachments") {
resultingType = noteContext.viewScope.attachmentId ? "attachmentDetail" : "attachmentList";
} else if (noteContext.viewScope?.viewMode === "note-map") {

View File

@@ -1,6 +1,6 @@
import "./PromotedAttributes.css";
import { UpdateAttributeResponse } from "@triliumnext/commons";
import { DefinitionObject, LabelType, UpdateAttributeResponse } from "@triliumnext/commons";
import clsx from "clsx";
import { ComponentChild, createElement, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -11,7 +11,7 @@ import FNote from "../entities/fnote";
import { Attribute } from "../services/attribute_parser";
import attributes from "../services/attributes";
import { t } from "../services/i18n";
import { DefinitionObject, extractAttributeDefinitionTypeAndName, LabelType } from "../services/promoted_attribute_definition_parser";
import { extractAttributeDefinitionTypeAndName } from "../services/promoted_attribute_definition_parser";
import server from "../services/server";
import { randomString } from "../services/utils";
import ws from "../services/ws";

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