Compare commits

...

539 Commits

Author SHA1 Message Date
Elian Doran
11d396016f refactor(collections/map): remove namespaced types 2026-02-16 22:53:51 +02:00
Elian Doran
d2cf8434cf chore(collections/map): make icon inside marker bigger 2026-02-16 22:46:58 +02:00
Elian Doran
5fec097ff8 feat(collections/map): add shadow to markers 2026-02-16 22:23:40 +02:00
Elian Doran
1812fdcfb6 chore(collections/map): remove circle from marker 2026-02-16 22:20:52 +02:00
Elian Doran
49c0adb5d3 chore(collections/map): reintroduce labels 2026-02-16 22:12:23 +02:00
Elian Doran
5c1c0efe0a fix(collections/map): markers loading slowly due to not waiting for styles 2026-02-16 21:43:02 +02:00
Elian Doran
a53dc1193b chore(collections/map): fix type issue 2026-02-16 20:37:50 +02:00
Elian Doran
0d169b0641 fix(collections/map): crash when reloading markers 2026-02-16 20:28:22 +02:00
Elian Doran
c080bb7926 refactor(collections/map): remove use of namespace in map.tsx 2026-02-16 20:06:04 +02:00
Elian Doran
693a9363a4 chore(client): types broken after removing import in type declaration 2026-02-16 20:05:31 +02:00
Elian Doran
7829957548 refactor(collections/map): extract markers into own component 2026-02-16 19:52:45 +02:00
Elian Doran
48ff643c7a refactor(collections/map): extract context menus to standalone component 2026-02-16 19:45:36 +02:00
Elian Doran
71e9eae5e6 fix(collections/map): tooltips not always available on first reload 2026-02-16 19:40:45 +02:00
Elian Doran
c7842c7f05 chore(collections/map): reintroduce tooltip on mouse enter 2026-02-16 19:01:40 +02:00
Elian Doran
b812d7f918 chore(collections/map): reintroduce marker context menu 2026-02-16 18:38:51 +02:00
Elian Doran
3041cb088c chore(collections/map): reintroduce zoom buttons 2026-02-16 18:18:13 +02:00
Elian Doran
638beb5b03 feat(collections/map): cache icon renderings 2026-02-15 21:37:41 +02:00
Elian Doran
156aa55ee7 refactor(collections/map): extract marker icon rendering 2026-02-15 21:23:22 +02:00
Elian Doran
f197756bb4 feat(collections/map): proper DPI scaling of icons 2026-02-15 21:19:19 +02:00
Elian Doran
ad1f83fc75 feat(collections/map): faster icon rendering 2026-02-15 21:05:14 +02:00
Elian Doran
12d9391b1e chore(collections/map): basic icon snapshot mechanism without cache 2026-02-15 20:27:11 +02:00
Elian Doran
60facceef1 chore(collections/map): bring back colors 2026-02-15 19:43:05 +02:00
Elian Doran
8d0c61ea37 refactor(collections/map): extract marker data build to separate module 2026-02-15 19:20:08 +02:00
Elian Doran
3865d96d44 feat(collections/map): use SVG for markers 2026-02-15 19:03:56 +02:00
Elian Doran
647ba85211 feat(collections/map): faster rendering of markers (use circle for now) 2026-02-15 18:57:42 +02:00
Elian Doran
ea09018acd Merge remote-tracking branch 'origin/main' into copilot/switch-to-mapbox-gl 2026-02-15 18:36:30 +02:00
Elian Doran
81445901fa Support for custom geomap tiles (#8703) 2026-02-15 18:21:49 +02:00
Elian Doran
e53fd1b059 Merge remote-tracking branch 'origin/main' into copilot/switch-to-mapbox-gl 2026-02-15 18:19:43 +02:00
Elian Doran
9e3bb585a2 fix(collections/map): shifting into collection properties when switching styles 2026-02-15 18:18:36 +02:00
Elian Doran
aa9b05641f fix(collections): collection properties still clipped in list/grid view 2026-02-15 18:16:15 +02:00
Elian Doran
cffcfeb734 feat(collections): improve display of no children 2026-02-15 18:15:24 +02:00
Elian Doran
6e32aaac33 fix(collections): list/grid collection properties clipped when little results 2026-02-15 18:14:02 +02:00
Elian Doran
f0d91d45f4 fix(collections): collection properties not shown if there are no children 2026-02-15 18:03:36 +02:00
Elian Doran
29ebb316b6 fix(map): crashing due to maxBounds 2026-02-15 17:57:58 +02:00
Elian Doran
e3172ebf1c fix(client): unreadable disabled item 2026-02-15 17:51:06 +02:00
Elian Doran
9b2876a8ff docs(user): mention custom tiles & hiding labels for geomap 2026-02-15 17:47:28 +02:00
Elian Doran
3db2c910e0 fix(client/active_badges): wrong enable + missing toggle for icon pack & custom CSS 2026-02-15 17:32:34 +02:00
Elian Doran
2799e4392f Merge remote-tracking branch 'origin/main' into feature/custom_map_improvements
; Conflicts:
;	apps/client/src/widgets/note_bars/CollectionProperties.tsx
;	apps/client/src/widgets/ribbon/collection-properties-config.tsx
2026-02-15 17:30:39 +02:00
Elian Doran
61963fcfda UX improvements for web view & render note (#8725) 2026-02-15 17:23:28 +02:00
Elian Doran
b0a3d54276 chore(client): address requested changes 2026-02-15 17:15:51 +02:00
Elian Doran
2135412c84 Translations update from Hosted Weblate (#8723) 2026-02-15 17:08:54 +02:00
Elian Doran
7a534c3ea7 chore(client/active_badges): disable toggle for web view & render note 2026-02-15 17:06:23 +02:00
Elian Doran
a6e596a5e3 feat(client/render_note): display rendering errors 2026-02-15 17:04:31 +02:00
Elian Doran
8b3e3c2c3a chore(react): not reacting properly to deleted relations 2026-02-15 16:56:37 +02:00
Elian Doran
ec8b0a3801 feat(client/render_note): warning for disabled render note 2026-02-15 16:54:34 +02:00
Elian Doran
197b769838 feat(client/render_note): create sample note with HTML 2026-02-15 16:46:19 +02:00
Elian Doran
1ba498c0e3 feat(client/render_note): create sample note with preact 2026-02-15 16:38:09 +02:00
Elian Doran
5550cb7b95 feat(client/render_note): basic setup for with note picker 2026-02-15 16:30:10 +02:00
Elian Doran
06a005acec refactor(client/render_note): separate content from setup 2026-02-15 16:17:41 +02:00
Elian Doran
2103be9d28 refactor(client): extract learn more into component 2026-02-15 16:11:52 +02:00
Elian Doran
a02f3c4440 refactor(client): extract setup form to dedicated component 2026-02-15 16:08:58 +02:00
Elian Doran
4b8d341e00 feat(web_view): add a screen if web view is disabled 2026-02-15 16:02:51 +02:00
Elian Doran
964633f426 refactor(client/web_view): fix lint & formatting 2026-02-15 15:36:58 +02:00
Marcel
d11fb38280 Translated using Weblate (German)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2026-02-15 13:34:30 +00:00
Elian Doran
04f4530990 Mobile improvements (#8724) 2026-02-15 15:34:20 +02:00
Elian Doran
5a77318a9e chore(mobile): address requested changes 2026-02-15 15:33:03 +02:00
Elian Doran
9ed2894a0c fix(mobile): badges not collapsing in quick edit 2026-02-15 15:29:27 +02:00
Elian Doran
e0766ad439 feat(mobile): display grid view on two columns 2026-02-15 15:25:28 +02:00
Elian Doran
67acfaab62 fix(mobile): missing scroll padding 2026-02-15 15:17:08 +02:00
Elian Doran
10b5d29107 fix(mobile): promoted attributes having a max-height 2026-02-15 15:16:23 +02:00
Elian Doran
297dd41170 fix(mobile): incorrect tab hue on light theme 2026-02-15 15:12:12 +02:00
Elian Doran
1364223599 fix(mobile): horizontall scroll in empty tab due to long attributes 2026-02-15 15:00:34 +02:00
Elian Doran
a6b7761dfa fix(mobile): double-scroll bar for empty tab 2026-02-15 14:58:28 +02:00
Elian Doran
2e6290c514 feat(mobile): improve empty tab workspace switcher 2026-02-15 14:55:01 +02:00
Elian Doran
78f4928611 feat(mobile): reduce margins for empty tab 2026-02-15 14:53:01 +02:00
Elian Doran
d044fce9c1 fix(mobile): remove redundant file properties footer 2026-02-15 14:44:21 +02:00
Elian Doran
11fa815e13 Active content badges (#8716) 2026-02-15 14:34:04 +02:00
Elian Doran
8d917eb970 Merge branch 'main' into feature/active_content_badges 2026-02-15 14:20:21 +02:00
Elian Doran
e411e5f2cf chore(client): fix typecheck 2026-02-15 14:20:09 +02:00
Elian Doran
48773636ca feat(collections): improve properties dropdown on mobile 2026-02-15 13:54:01 +02:00
Elian Doran
5a3c7355c1 chore(badges/content): improve dropdown on mobile 2026-02-15 13:51:48 +02:00
Elian Doran
afa9fe3063 refactor(badges/content): integrate title into definition 2026-02-15 13:46:47 +02:00
Elian Doran
178ac088b4 refactor(badges/content): integrate widget switcher using additional options 2026-02-15 13:44:16 +02:00
Elian Doran
ffd5ebbe79 refactor(badges/content): integrate execute using additional options 2026-02-15 13:38:02 +02:00
Elian Doran
6b78bfecb4 refactor(badges/content): integrate run options using additional options 2026-02-15 13:34:48 +02:00
Elian Doran
d3c0a44c00 chore(badges/content): remove the base attribute for legacy 2026-02-15 13:26:23 +02:00
Elian Doran
33ea2de231 chore(badges/content): use translations 2026-02-15 13:23:36 +02:00
Elian Doran
43ebbfc321 chore(badges/content): add separator 2026-02-15 13:21:29 +02:00
Elian Doran
1e5b294eb3 feat(badges/content): adjustable base for app theme 2026-02-15 13:19:12 +02:00
Elian Doran
29855112c8 refactor(client): move note properties mapping outside collection properties 2026-02-15 13:13:18 +02:00
Elian Doran
8af7b3c81a feat(badges/content): make app theme toggleable 2026-02-15 12:42:46 +02:00
Elian Doran
b72b82ff1a feat(badges/content): add badge for app themes 2026-02-15 12:37:37 +02:00
Elian Doran
1588c8103c docs(user): improve documentation on scripts & active content 2026-02-15 12:32:41 +02:00
Elian Doran
15e569dcea chore(client): address requested changes 2026-02-15 12:32:30 +02:00
Elian Doran
4bdb407404 Translations update from Hosted Weblate (#8718) 2026-02-14 22:16:09 +02:00
Elian Doran
4568cedcd3 chore(deps): update dependency @redocly/cli to v2.18.1 (#8706) 2026-02-14 22:05:15 +02:00
AggelosPnS
f290317acc Translated using Weblate (Greek)
Currently translated at 12.3% (48 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/el/
2026-02-14 20:03:29 +00:00
AggelosPnS
645279c8fa Translated using Weblate (Greek)
Currently translated at 2.7% (49 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-02-14 20:03:28 +00:00
Hosted Weblate
09436f8d65 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-14 20:03:27 +00:00
Elian Doran
b616e0e5f9 chore(deps): update dependency electron to v40.4.1 (#8707) 2026-02-14 22:03:19 +02:00
Elian Doran
f06a0852a1 chore(deps): update dependency typedoc to v0.28.17 (#8708) 2026-02-14 22:02:02 +02:00
Elian Doran
9e688138be fix(deps): update dependency i18next to v25.8.7 (#8709) 2026-02-14 21:56:00 +02:00
Elian Doran
5d46970a38 fix(deps): update dependency react-window to v2.2.7 (#8710) 2026-02-14 21:54:40 +02:00
Elian Doran
ecc441c074 chore(deps): update dependency openai to v6.22.0 (#8711) 2026-02-14 21:53:59 +02:00
Elian Doran
f2ce3678c4 chore(deps): update dependency stylelint to v17.3.0 (#8712) 2026-02-14 21:53:41 +02:00
Elian Doran
c9ad390647 fix(deps): update dependency @preact/signals to v2.8.0 (#8713) 2026-02-14 21:52:55 +02:00
Elian Doran
92acc7accd Translations update from Hosted Weblate (#8715) 2026-02-14 21:52:11 +02:00
Elian Doran
2ef4eb7eae chore(client): fix type errors 2026-02-14 12:43:40 +02:00
Elian Doran
7b230706cb feat(badges/content): improve color support 2026-02-14 12:41:41 +02:00
Elian Doran
5a2b04adba feat(badges/content): add support for web view 2026-02-14 12:35:06 +02:00
Elian Doran
50dcd3ba44 feat(badges/content): add support for render note 2026-02-14 12:30:43 +02:00
Elian Doran
740b02952f feat(badges/content): disable toggle when not necessary 2026-02-14 12:22:27 +02:00
Aindriú Mac Giolla Eoin
5d3d42ffdd Translated using Weblate (Irish)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-14 10:09:46 +00:00
Elian Doran
866d3110da feat(badges/content): add badge for custom CSS 2026-02-14 12:05:24 +02:00
Elian Doran
7a3e7fccec feat(badges/content): handle widgets as separate content type 2026-02-14 11:56:40 +02:00
Elian Doran
3107bc8840 feat(badges/content): add toggle for widget 2026-02-14 11:47:42 +02:00
Elian Doran
2d34cdef5e feat(badges/content): integrate options for frontend script 2026-02-14 11:37:20 +02:00
Elian Doran
bd1f0909a2 feat(badges/content): configurable backend run options 2026-02-14 11:30:05 +02:00
Elian Doran
ef75de63fe feat(badges/content): option to execute now 2026-02-14 11:15:08 +02:00
Elian Doran
a739d28563 feat(badges/content): option to open API docs 2026-02-14 11:12:55 +02:00
Elian Doran
66ff009b72 feat(badges/content): option to open documentation 2026-02-14 11:08:30 +02:00
Elian Doran
a68e82c1c8 feat(badges/content): basic support for backend scripts 2026-02-14 10:57:32 +02:00
Elian Doran
46556c1c14 chore(badges/content): make toggle more compact 2026-02-14 10:40:19 +02:00
Elian Doran
7be637798f feat(badges/content): functional enable/disable toggle 2026-02-14 10:30:34 +02:00
Elian Doran
9b3396349e refactor(commons): add builtin_attributes to commons 2026-02-14 10:30:24 +02:00
Elian Doran
ccff210b4c feat(badges/content): indicate enabled/disabled state 2026-02-14 09:57:40 +02:00
Elian Doran
a2264847b6 refactor(badges/content): use shared mechanism for extracting info 2026-02-14 09:53:37 +02:00
Elian Doran
7d103f8c50 refactor(badges/content): extract to separate file 2026-02-14 09:37:48 +02:00
Elian Doran
f3dccc0aec feat(badges/content): detect icon pack 2026-02-14 09:19:46 +02:00
renovate[bot]
311b1d8a64 fix(deps): update dependency @preact/signals to v2.8.0 2026-02-14 02:08:43 +00:00
renovate[bot]
f3094e3079 chore(deps): update dependency stylelint to v17.3.0 2026-02-14 02:08:04 +00:00
renovate[bot]
f3b37b16d5 chore(deps): update dependency openai to v6.22.0 2026-02-14 02:07:20 +00:00
renovate[bot]
5f16ecf02d fix(deps): update dependency react-window to v2.2.7 2026-02-14 02:06:40 +00:00
renovate[bot]
8b75287827 fix(deps): update dependency i18next to v25.8.7 2026-02-14 02:05:55 +00:00
renovate[bot]
bb38e806cd chore(deps): update dependency typedoc to v0.28.17 2026-02-14 02:05:04 +00:00
renovate[bot]
8cbc15f1b0 chore(deps): update dependency electron to v40.4.1 2026-02-14 02:04:20 +00:00
renovate[bot]
870524f9cf chore(deps): update dependency @redocly/cli to v2.18.1 2026-02-14 02:03:29 +00:00
Elian Doran
5da9963f31 fix(website): web clipper URL incorrect 2026-02-13 19:43:07 +02:00
Elian Doran
34e885812f fix(global_menu): advanced menu not accessible in other languages (closes #8694) 2026-02-13 19:40:51 +02:00
Elian Doran
a9ac11452d fix(relation_map): crash when valid JSON, but missing data (closes #8702) 2026-02-13 19:32:29 +02:00
Elian Doran
04b91308b1 chore(search): remove redundant border on mobile 2026-02-13 19:26:08 +02:00
Elian Doran
b09ef222f5 fix(search): errors not displayed (closes #8704) 2026-02-13 19:22:21 +02:00
Elian Doran
2d0ed06d50 Update Node.js to v24.13.1 (#8629) 2026-02-13 13:23:22 +02:00
renovate[bot]
8dd7cf6085 Update Node.js to v24.13.1 2026-02-13 05:50:03 +00:00
Elian Doran
4999bd4f1e Translations update from Hosted Weblate (#8701) 2026-02-13 07:48:43 +02:00
green
22f408addb Translated using Weblate (Japanese)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-13 06:48:06 +01:00
AggelosPnS
9ca1dbe638 Translated using Weblate (Greek)
Currently translated at 8.9% (35 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/el/
2026-02-13 06:48:06 +01:00
AggelosPnS
26662952e3 Translated using Weblate (Greek)
Currently translated at 2.0% (36 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/el/
2026-02-13 06:48:06 +01:00
Hosted Weblate
f0c9fa4ca3 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-13 06:48:06 +01:00
Elian Doran
b51aa1dd71 Update pnpm to v10.29.3 (#8683) 2026-02-13 07:48:00 +02:00
Elian Doran
846253c9e3 Update dependency @codemirror/view to v6.39.14 (#8695) 2026-02-13 07:47:46 +02:00
Elian Doran
6ab6ea97ac Update dependency i18next to v25.8.6 (#8696) 2026-02-13 07:47:31 +02:00
Elian Doran
bf41f70b98 Update dependency wxt to v0.20.17 (#8697) 2026-02-13 07:46:48 +02:00
Elian Doran
67ddbedd08 Update dependency dotenv to v17.3.1 (#8698) 2026-02-13 07:45:17 +02:00
renovate[bot]
2573e219dc Update dependency dotenv to v17.3.1 2026-02-13 01:02:52 +00:00
renovate[bot]
7e368678ab Update dependency wxt to v0.20.17 2026-02-13 01:02:01 +00:00
renovate[bot]
4a9fcf7ab6 Update dependency i18next to v25.8.6 2026-02-13 01:01:10 +00:00
renovate[bot]
65856c61c5 Update dependency @codemirror/view to v6.39.14 2026-02-13 01:00:19 +00:00
copilot-swe-agent[bot]
89ea0a2730 Replace Leaflet with MapLibre GL for geo map collection
- Remove leaflet, leaflet-gpx, @maplibre/maplibre-gl-leaflet and related @types packages
- Add maplibre-gl 5.18.0 as dependency
- Rewrite map.tsx to use MapLibre GL directly (native vector+raster support)
- Rewrite marker.tsx with MapLibre GL HTML markers
- Rewrite GPX track rendering using DOMParser + GeoJSON layers
- Update api.ts, context_menu.ts, index.tsx, map_layer.ts to use GeoMouseEvent
- Update link_context_menu.ts to remove LeafletMouseEvent dependency
- Update types-lib.d.ts to remove Leaflet module augmentation
- Update CSS for MapLibre GL marker classes
- Replace Leaflet marker PNG with inline SVG marker pin

Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
2026-02-12 20:26:37 +00:00
copilot-swe-agent[bot]
d847d966bc Initial plan 2026-02-12 20:16:25 +00:00
renovate[bot]
b5a97bffab Update pnpm to v10.29.3 2026-02-12 17:58:49 +00:00
Elian Doran
e6d728715f feat(geomap): support hiding labels 2026-02-12 19:47:24 +02:00
Elian Doran
54a52f0589 feat(geomap): support for custom tile URLs 2026-02-12 19:32:22 +02:00
Elian Doran
badfa23f86 refactor(geomap): delegate layer data handling to index 2026-02-12 19:12:56 +02:00
Elian Doran
30ccd3487a Translations update from Hosted Weblate (#8666) 2026-02-12 07:50:46 +02:00
Toto Yullian
75e012f2c9 Translated using Weblate (Indonesian)
Currently translated at 3.5% (63 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/id/
2026-02-12 06:48:36 +01:00
Ulices
5ecb1d1e2d Translated using Weblate (Spanish)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:36 +01:00
Toto Yullian
f8c24c838a Translated using Weblate (Indonesian)
Currently translated at 41.1% (65 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/id/
2026-02-12 06:48:35 +01:00
noobhjy
4ad9cfcdf4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:34 +01:00
Toto Yullian
a57253dd35 Translated using Weblate (Indonesian)
Currently translated at 65.5% (76 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-02-12 06:48:33 +01:00
Marcel
222e65bd45 Translated using Weblate (German)
Currently translated at 100.0% (1777 of 1777 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-12 06:48:32 +01:00
Jason Kuanca
3192ea3383 Translated using Weblate (Indonesian)
Currently translated at 60.3% (70 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/id/
2026-02-12 06:48:32 +01:00
green
3a27f873cd Translated using Weblate (Japanese)
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-12 06:48:31 +01:00
noobhjy
e8fb279036 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:30 +01:00
Ulices
21ec7078d2 Translated using Weblate (Spanish)
Currently translated at 100.0% (1776 of 1776 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:29 +01:00
Aindriú Mac Giolla Eoin
94937e9fa4 Translated using Weblate (Irish)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-12 06:48:28 +01:00
Giovi
36401a20b8 Translated using Weblate (Italian)
Currently translated at 99.7% (1769 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/it/
2026-02-12 06:48:27 +01:00
noobhjy
8d1c4e4661 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-12 06:48:27 +01:00
Kuzma Simonov
14362060c8 Translated using Weblate (Russian)
Currently translated at 99.3% (1763 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ru/
2026-02-12 06:48:26 +01:00
Kuzma Simonov
de47e94f62 Translated using Weblate (Russian)
Currently translated at 99.3% (157 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ru/
2026-02-12 06:48:25 +01:00
Giovi
c78ed78bf6 Translated using Weblate (Italian)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/it/
2026-02-12 06:48:24 +01:00
Ulices
7674c95124 Translated using Weblate (Spanish)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-12 06:48:24 +01:00
Giovi
ec522c20b2 Translated using Weblate (Italian)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/it/
2026-02-12 06:48:23 +01:00
green
81f9578526 Translated using Weblate (Japanese)
Currently translated at 100.0% (1774 of 1774 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-12 06:48:22 +01:00
Hosted Weblate
20bca751d4 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2026-02-12 06:48:22 +01:00
Francis C.
49b5c49776 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/zh_Hant/
2026-02-12 06:48:20 +01:00
Francis C.
5d514fae61 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-02-12 06:48:20 +01:00
Hosted Weblate
9e17e93dd7 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-12 06:48:19 +01:00
Elian Doran
7f70c641dc chore(deps): update dependency dotenv to v17.2.4 (#8630) 2026-02-12 07:47:59 +02:00
Elian Doran
7e3af4b7bc chore(deps): update dependency happy-dom to v20.6.1 (#8662) 2026-02-12 07:47:18 +02:00
Elian Doran
59ff4c0aef chore(deps): update dependency wxt to v0.20.15 (#8682) 2026-02-12 07:46:46 +02:00
Elian Doran
875e6b8e53 fix(deps): update dependency i18next to v25.8.5 (#8685) 2026-02-12 07:45:10 +02:00
Elian Doran
fafc44de7c fix(deps): update dependency marked to v17.0.2 (#8686) 2026-02-12 07:44:53 +02:00
Elian Doran
e0d7eb10d5 fix(deps): update dependency mathlive to v0.108.3 (#8687) 2026-02-12 07:44:40 +02:00
Elian Doran
ae01eb28a3 chore(deps): update dependency @redocly/cli to v2.18.0 (#8688) 2026-02-12 07:44:20 +02:00
Elian Doran
637c66c04f chore(deps): update dependency electron to v40.4.0 (#8689) 2026-02-12 07:44:01 +02:00
renovate[bot]
1c061e4428 chore(deps): update dependency electron to v40.4.0 2026-02-12 02:18:16 +00:00
renovate[bot]
cbe0626572 chore(deps): update dependency @redocly/cli to v2.18.0 2026-02-12 02:17:42 +00:00
renovate[bot]
86dc98174a fix(deps): update dependency mathlive to v0.108.3 2026-02-12 02:17:07 +00:00
renovate[bot]
f0ac8ea977 fix(deps): update dependency marked to v17.0.2 2026-02-12 02:16:29 +00:00
renovate[bot]
35d4c2cdfc fix(deps): update dependency i18next to v25.8.5 2026-02-12 02:15:53 +00:00
renovate[bot]
394f7c0d09 chore(deps): update dependency wxt to v0.20.15 2026-02-12 02:13:10 +00:00
Elian Doran
b83eee9bdc Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-11 20:21:55 +02:00
Elian Doran
e41b2e8d31 fix(options/shortcuts): filter text box forces lower case 2026-02-11 20:21:52 +02:00
Elian Doran
d93cec2bfd feat(options/shortcuts): add no results 2026-02-11 20:19:53 +02:00
Elian Doran
9eb87a39cd chore(options/shortcuts): refactor filtering 2026-02-11 20:15:39 +02:00
Elian Doran
07818ec1df fix(options): unnecessary full-height 2026-02-11 20:11:46 +02:00
Elian Doran
d4052dbe37 feat(options/shortcuts): equal height 2026-02-11 20:04:09 +02:00
Elian Doran
656f5e0a7f feat(options/shortcuts): make header and footer sticky (closes #8675) 2026-02-11 20:02:37 +02:00
Elian Doran
0cc5e4dac3 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v54.3.3 (#8609) 2026-02-11 07:51:46 +02:00
Elian Doran
8bbfff3cb2 fix(deps): update dependency i18next to v25.8.4 (#8611) 2026-02-11 07:51:31 +02:00
renovate[bot]
93059798b0 chore(deps): update dependency dotenv to v17.2.4 2026-02-11 05:51:15 +00:00
Elian Doran
33fae88cad chore(deps): update dependency stylelint to v17.2.0 (#8612) 2026-02-11 07:51:15 +02:00
Elian Doran
4feb23e9ca fix(deps): update dependency @preact/signals to v2.7.1 (#8619) 2026-02-11 07:50:04 +02:00
Elian Doran
c2b6b7ba72 chore(deps): update dependency esbuild to v0.27.3 (#8631) 2026-02-11 07:48:22 +02:00
Elian Doran
fb76aee258 chore(deps): update dependency @anthropic-ai/sdk to v0.74.0 (#8633) 2026-02-11 07:47:46 +02:00
Elian Doran
320b1829cc chore(deps): update dependency openai to v6.21.0 (#8634) 2026-02-11 07:47:16 +02:00
Elian Doran
601f0255a4 chore(deps): update dependency @playwright/test to v1.58.2 (#8642) 2026-02-11 07:46:46 +02:00
Elian Doran
b6cc2b227a chore(deps): update dependency wxt to v0.20.14 (#8643) 2026-02-11 07:46:36 +02:00
renovate[bot]
cc7da4b948 chore(deps): update dependency happy-dom to v20.6.1 2026-02-11 05:46:19 +00:00
Elian Doran
1ae3be2fda fix(deps): update codemirror (#8644) 2026-02-11 07:44:55 +02:00
Elian Doran
5b4d35ea86 chore(deps): update dependency @redocly/cli to v2.17.0 (#8645) 2026-02-11 07:44:38 +02:00
Elian Doran
00735e6c8e chore(deps): update dependency axios to v1.13.5 (#8661) 2026-02-11 07:44:27 +02:00
Elian Doran
66a42a38c9 chore(deps): update dependency @smithy/middleware-retry to v4.4.31 (#8669) 2026-02-11 07:43:54 +02:00
Elian Doran
05af1fba80 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.11 (#8670) 2026-02-11 07:43:45 +02:00
Elian Doran
3b2dd0f5e9 chore(deps): update pnpm to v10.29.2 (#8671) 2026-02-11 07:43:33 +02:00
Elian Doran
1493a66a36 chore(deps): update dependency webdriverio to v9.24.0 (#8672) 2026-02-11 07:43:15 +02:00
Elian Doran
b4bf103fd8 chore(deps): update typescript-eslint monorepo to v8.55.0 (#8673) 2026-02-11 07:43:00 +02:00
renovate[bot]
94286becfd chore(deps): update typescript-eslint monorepo to v8.55.0 2026-02-11 01:04:05 +00:00
renovate[bot]
a68aade58c chore(deps): update dependency webdriverio to v9.24.0 2026-02-11 01:03:25 +00:00
renovate[bot]
014201edf4 chore(deps): update pnpm to v10.29.2 2026-02-11 01:02:09 +00:00
renovate[bot]
8ae6297148 chore(deps): update dependency rollup-plugin-webpack-stats to v2.1.11 2026-02-11 01:01:56 +00:00
renovate[bot]
5e0300aa8e chore(deps): update dependency @smithy/middleware-retry to v4.4.31 2026-02-11 01:01:18 +00:00
renovate[bot]
80a7e18413 chore(deps): update dependency openai to v6.21.0 2026-02-10 21:08:56 +00:00
Elian Doran
43be0a1a3f chore(desktop): disable dev tools for the print window 2026-02-10 17:29:00 +02:00
Elian Doran
5eb32744c3 feat(desktop): display print errors 2026-02-10 17:26:58 +02:00
Elian Doran
89d39f5f2b feat(desktop): add logs when printing 2026-02-10 17:12:28 +02:00
Elian Doran
1f4900dd1e fix(desktop): print failing on upgrade due to cache issues 2026-02-10 16:30:16 +02:00
renovate[bot]
1c561c1483 chore(deps): update dependency stylelint to v17.2.0 2026-02-10 11:44:18 +00:00
Adorian Doran
6baaf60b67 style/web view setup: update icon 2026-02-10 02:53:46 +02:00
Adorian Doran
dde73f6c2b style/attachments: tweak 2026-02-10 02:46:10 +02:00
Adorian Doran
f445a49b34 client/notes/web view: create a web view setup form 2026-02-10 02:45:43 +02:00
Adorian Doran
29016d1cf5 style/refactor: promote centered form as global style 2026-02-10 01:26:07 +02:00
Adorian Doran
c06435046b style/alert bars: use a global style, tweak spacing 2026-02-09 23:39:22 +02:00
Adorian Doran
134422802f style/protected session password request: improve layout and appearance 2026-02-09 23:14:32 +02:00
Adorian Doran
5e00d6a305 style/collapsible: use low profile style for the expand/collapse button 2026-02-09 20:34:58 +02:00
Adorian Doran
b5f0137d8e Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-09 20:33:32 +02:00
Adorian Doran
081d041cbc style/buttons: define a style for low profile buttons 2026-02-09 20:33:24 +02:00
Elian Doran
95e733a67c Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-09 20:22:43 +02:00
Elian Doran
a664057312 fix(website): missing RPM GPG key (closes #8618) 2026-02-09 20:22:41 +02:00
Elian Doran
5b1a2d93bf fix(tree): child note badge overlapping text (closes #8567) 2026-02-09 20:20:20 +02:00
Adorian Doran
0323f95828 style/status bar: do not wrap the action buttons text 2026-02-09 19:57:13 +02:00
Elian Doran
375838449f fix(geomap): z-index issues with toolbar and buttons (closes #8649) 2026-02-09 19:55:57 +02:00
Adorian Doran
4562de8c2c close #2137 2026-02-09 19:19:41 +02:00
Adorian Doran
68d21669e7 style/similar notes: fix font size variation according to similarity 2026-02-09 19:12:47 +02:00
renovate[bot]
625e0cf159 chore(deps): update dependency @redocly/cli to v2.17.0 2026-02-09 13:58:05 +00:00
renovate[bot]
551ef00c61 fix(deps): update dependency @preact/signals to v2.7.1 2026-02-09 10:46:41 +00:00
renovate[bot]
10518f6364 chore(deps): update dependency axios to v1.13.5 2026-02-09 00:39:09 +00:00
Elian Doran
1eafda36a9 Translations update from Hosted Weblate (#8652) 2026-02-08 21:02:38 +02:00
TS
871ecf0158 Translated using Weblate (Polish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/pl/
2026-02-08 18:57:05 +00:00
TS
429000bdcb Translated using Weblate (Polish)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/pl/
2026-02-08 18:57:04 +00:00
TS
607940ed60 Translated using Weblate (Polish)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/pl/
2026-02-08 18:57:03 +00:00
noobhjy
46f61a4311 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/zh_Hans/
2026-02-08 18:57:02 +00:00
Marcel
3721df0502 Translated using Weblate (German)
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-08 18:57:02 +00:00
Marcel
11add681ec Translated using Weblate (German)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2026-02-08 18:57:01 +00:00
TS
6e36eea6c8 Translated using Weblate (Polish)
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/pl/
2026-02-08 18:57:00 +00:00
Aindriú Mac Giolla Eoin
6a82e7a24c Translated using Weblate (Irish)
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-08 18:56:59 +00:00
Ulices
5f625fa9f3 Translated using Weblate (Spanish)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/es/
2026-02-08 18:56:58 +00:00
Aindriú Mac Giolla Eoin
a95527674f Translated using Weblate (Irish)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ga/
2026-02-08 18:56:58 +00:00
green
f6149c67dc Translated using Weblate (Japanese)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ja/
2026-02-08 18:56:57 +00:00
Ulices
00c2a07e33 Translated using Weblate (Spanish)
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-08 18:56:56 +00:00
Elian Doran
bc49b22c32 docs(user): add note about icon packs 2026-02-08 20:55:48 +02:00
Elian Doran
fe1b1c8bc3 feat(website/resources): add note about icon packs 2026-02-08 20:52:13 +02:00
Elian Doran
a2825a06d6 fix(website): top level subpages not rendered 2026-02-08 20:43:48 +02:00
renovate[bot]
3c33b5b169 fix(deps): update codemirror 2026-02-08 13:39:10 +00:00
Elian Doran
934a867c83 chore(deps): update dependency electron to v40.2.1 (#8646) 2026-02-08 10:42:50 +02:00
Elian Doran
a36337fba8 chore(deps): update pnpm to v10.29.1 (#8655) 2026-02-08 10:42:17 +02:00
Elian Doran
b3bd53bdd0 fix(deps): update dependency mind-elixir to v5.8.0 (#8656) 2026-02-08 10:41:23 +02:00
Elian Doran
55518d4a8e fix(deps): update dependency eslint-linter-browserify to v10 (#8657) 2026-02-08 10:39:34 +02:00
renovate[bot]
2949c330d7 fix(deps): update dependency eslint-linter-browserify to v10 2026-02-08 00:28:40 +00:00
renovate[bot]
7ec056dbe0 fix(deps): update dependency mind-elixir to v5.8.0 2026-02-08 00:28:02 +00:00
renovate[bot]
983b60a8b9 chore(deps): update pnpm to v10.29.1 2026-02-08 00:27:27 +00:00
renovate[bot]
000c31b66c chore(deps): update dependency @anthropic-ai/sdk to v0.74.0 2026-02-07 20:46:58 +00:00
Elian Doran
2cd3d4bfb7 Simple content library for icon packs (#8650) 2026-02-07 16:52:08 +02:00
Elian Doran
9d9065c801 feat(website): add GitHub link on mid-size 2026-02-07 16:42:40 +02:00
Elian Doran
10e7fe56ab feat(website): add GitHub link on mobile 2026-02-07 16:36:53 +02:00
Elian Doran
c832e62389 fix(website): page not rendering properly on cold refresh 2026-02-07 16:29:34 +02:00
Elian Doran
328e488322 chore(website): address requested changes 2026-02-07 16:18:51 +02:00
Elian Doran
fd5a43cdb4 Translations update from Hosted Weblate (#8648) 2026-02-07 13:54:22 +02:00
renovate[bot]
fd7780abb0 chore(deps): update dependency esbuild to v0.27.3 2026-02-07 11:53:22 +00:00
Elian Doran
11c23e0b5e chore(deps): update dependency eslint to v10 (#8647) 2026-02-07 13:51:39 +02:00
Elian Doran
1170cfd4a7 fix(website): stargrazers count still not updating due to hydration 2026-02-07 13:47:25 +02:00
Elian Doran
2eddaa954e chore: address requested changes 2026-02-07 12:38:41 +02:00
Elian Doran
2c472fd03f docs(user): improve docuemntation slightly on icon packs 2026-02-07 12:37:10 +02:00
Elian Doran
16019a787e fix(website): stargazers count not updating 2026-02-07 12:27:16 +02:00
Elian Doran
717d0a75f4 chore(website): improve fit for full-height section 2026-02-07 12:20:24 +02:00
Elian Doran
cc9487bae8 chore(website): fix warnings & type issue 2026-02-07 12:17:14 +02:00
Elian Doran
5ed9ec8f46 fix(website): unnecessary horizontal scroll on md screen 2026-02-07 12:11:41 +02:00
Elian Doran
6ca37ca7f4 feat(header): improve header fit on smaller screens 2026-02-07 11:56:51 +02:00
Elian Doran
1007b8b15d fix(website): subpixel scaling causing issues at 719px 2026-02-07 11:47:30 +02:00
Elian Doran
be3a95fd54 chore(website): add resources to header 2026-02-07 11:19:46 +02:00
Elian Doran
109cb6cc3f chore(website/resources): add descriptions to fonts 2026-02-07 11:18:05 +02:00
Elian Doran
fe1509dcfc chore(website/resources): display version slightly smaller 2026-02-07 11:09:09 +02:00
Elian Doran
1797e33989 feat(website/resources): add intro blurb for icon packs 2026-02-07 11:08:03 +02:00
Elian Doran
6c504eeb3e feat(website/resources): make downloadable 2026-02-07 10:52:23 +02:00
Elian Doran
d4b16fcdd1 feat(website/resources): display icon packs 2026-02-07 10:36:00 +02:00
Elian Doran
9a08c079b5 chore(icon-pack-builder): build directly in website resource path 2026-02-07 10:24:32 +02:00
Elian Doran
98e75a7d6c chore(icon-pack-builder): generate meta alongside files 2026-02-07 10:17:58 +02:00
Elian Doran
675fd13391 chore(icon-pack-builder): add website and version meta 2026-02-07 10:15:55 +02:00
Elian Doran
e4f042aba4 fix(content_renderer): prevent iconPacks from rendering in preview 2026-02-07 09:59:53 +02:00
Elian Doran
19527845d1 fix(tree): wrong icon on multi selection with custom icons 2026-02-07 09:54:50 +02:00
Elian Doran
3aa981649c fix(icon-pack-builder): wrong icon for boxicons3-brands 2026-02-07 09:51:21 +02:00
Elian Doran
330d48f70d chore(website): create empty resources page 2026-02-07 09:42:42 +02:00
noobhjy
90e14aae99 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-07 02:01:52 +01:00
green
77e2ed7e01 Translated using Weblate (Japanese)
Currently translated at 100.0% (1772 of 1772 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-07 02:01:50 +01:00
renovate[bot]
f720f921c3 chore(deps): update dependency eslint to v10 2026-02-07 00:36:40 +00:00
renovate[bot]
328740909b chore(deps): update dependency electron to v40.2.1 2026-02-07 00:35:41 +00:00
renovate[bot]
c8a981e8d6 chore(deps): update dependency wxt to v0.20.14 2026-02-07 00:33:00 +00:00
renovate[bot]
faac45784c chore(deps): update dependency @playwright/test to v1.58.2 2026-02-07 00:32:04 +00:00
Elian Doran
25667e84b7 fix(deps): update dependency force-graph to v1.51.1 (#8632) 2026-02-06 22:40:03 +02:00
Elian Doran
72e0d77be5 chore(i18n): fix more issues related to Irish 2026-02-06 21:44:39 +02:00
Elian Doran
9ac4b9ed4f docs(dev): refresh adding new locale 2026-02-06 21:18:29 +02:00
Elian Doran
b3b89ba05c chore(i18n): fix calendar mapping for Irish 2026-02-06 21:16:07 +02:00
Elian Doran
00dc04df25 chire(pdfjs): fix handling of Irish 2026-02-06 20:46:26 +02:00
Elian Doran
21d47c3fef chore(i18n): fix issues with ga 2026-02-06 20:24:16 +02:00
Elian Doran
66de94f050 feat(i18n): enable Irish language 2026-02-06 19:56:37 +02:00
Elian Doran
1917adb322 chore(scripts): improve messages for translation check script 2026-02-06 19:38:24 +02:00
Elian Doran
3360b29354 Translations update from Hosted Weblate (#8599) 2026-02-06 10:32:09 +02:00
aloeaqua
646d281759 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-02-05 21:44:58 +00:00
aloeaqua
e268d92d52 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2026-02-05 21:44:57 +00:00
Aindriú Mac Giolla Eoin
fa81db2f03 Translated using Weblate (Irish)
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-05 21:44:56 +00:00
Marcel
830199ba3a Translated using Weblate (German)
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-05 21:44:56 +00:00
Ulices
ea8dc506d3 Translated using Weblate (Spanish)
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-05 21:44:55 +00:00
ibs-allaow
c95483ed94 Translated using Weblate (Arabic)
Currently translated at 49.1% (57 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2026-02-05 21:44:54 +00:00
ibs-allaow
d9cb4480b2 Translated using Weblate (Arabic)
Currently translated at 59.8% (1058 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-02-05 21:44:53 +00:00
ibs-allaow
c69afd6074 Translated using Weblate (Arabic)
Currently translated at 64.4% (98 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ar/
2026-02-05 21:44:53 +00:00
ibs-allaow
f541790ab4 Translated using Weblate (Arabic)
Currently translated at 48.2% (56 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2026-02-05 21:44:52 +00:00
green
707ac4ec36 Translated using Weblate (Japanese)
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-05 21:44:51 +00:00
noobhjy
cd55133e96 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1768 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-05 21:44:50 +00:00
ibs-allaow
37a91f8529 Translated using Weblate (Arabic)
Currently translated at 59.7% (1057 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-02-05 21:44:49 +00:00
Aindriú Mac Giolla Eoin
0ed0fa37a1 Translated using Weblate (Irish)
Currently translated at 55.4% (981 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-05 21:44:48 +00:00
ibs-allaow
26728b79b2 Translated using Weblate (Arabic)
Currently translated at 59.7% (1056 of 1768 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-02-05 21:44:47 +00:00
ibs-allaow
f7c8723539 Translated using Weblate (Arabic)
Currently translated at 63.1% (96 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ar/
2026-02-05 21:44:47 +00:00
Hosted Weblate
c313916771 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-05 21:44:46 +00:00
Elian Doran
f0b30c5e91 Mobile improvements v2 (#8615) 2026-02-05 23:44:31 +02:00
Elian Doran
73e3196124 docs(user): improve documentation on mobile support 2026-02-05 23:31:12 +02:00
Elian Doran
8a7bcc316e chore(mobile): address requested changes 2026-02-05 22:25:01 +02:00
Elian Doran
a2921cb982 fix(mobile): missing snap in board view 2026-02-05 22:15:41 +02:00
Elian Doran
29ce004974 chore(mobile): add possible work-around for note switcher sometimes disappearing 2026-02-05 22:13:09 +02:00
Elian Doran
026ba5ddce chore(mobile/attachments): improve layout 2026-02-05 22:02:11 +02:00
Elian Doran
ab0585609a chore(mobile/attachments): use bottom-style menu for actions 2026-02-05 21:57:49 +02:00
Elian Doran
14a8bdb0c0 chore(launch_bar): add backdrop on mobile for dropdown menus 2026-02-05 21:53:54 +02:00
Elian Doran
397d04dd88 style(client): fix rounded corners in launcher bar dropdown 2026-02-05 21:48:38 +02:00
Elian Doran
fbb0bb7491 Revert "feat(mobile): make the sidebar gesture easier to press"
This reverts commit f8b386e42d.
2026-02-05 21:47:40 +02:00
Elian Doran
ee987dae99 refactor(client): fix typo in container 2026-02-05 21:44:48 +02:00
Elian Doran
720281a8db feat(bookmarks): support bookmark folders on mobile 2026-02-05 21:43:58 +02:00
Elian Doran
ff0c89e5a3 feat(bookmarks): collapse on mobile into single icon (closes #5464) 2026-02-05 21:28:50 +02:00
Elian Doran
442937f540 feat(bookmarks): add launcher on mobile 2026-02-05 21:20:22 +02:00
Elian Doran
dc01b787c1 fix(launch_bar): cannot create new items in mobile launch bar (fixes #8054) 2026-02-05 21:10:05 +02:00
Elian Doran
c709b5d34c chore(mobile): relocate some note actions 2026-02-05 18:42:51 +02:00
Elian Doran
f8b386e42d feat(mobile): make the sidebar gesture easier to press 2026-02-05 16:43:06 +02:00
renovate[bot]
f6b454cb9a fix(deps): update dependency i18next to v25.8.4 2026-02-05 13:44:21 +00:00
Elian Doran
a82f8ce3ad Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2 2026-02-05 10:28:02 +02:00
Elian Doran
529d45b762 chore(deps): update dependency @types/node to v24.10.10 (#8610) 2026-02-05 09:45:49 +02:00
Elian Doran
d31135bf21 fix(mobile): status bar color for Samsung Internet 2026-02-05 09:09:47 +02:00
Elian Doran
75a77acefe Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2 2026-02-05 09:09:41 +02:00
renovate[bot]
715f42b6c3 fix(deps): update dependency force-graph to v1.51.1 2026-02-05 01:13:22 +00:00
Elian Doran
199bfc8a37 Adjust next theme development docu (#8616) 2026-02-04 21:50:36 +02:00
hulmgulm
e82ae762f0 More fixes 2026-02-04 20:48:36 +01:00
hulmgulm
f90cc9aff7 Adjust theme development docu 2026-02-04 20:39:58 +01:00
Elian Doran
ec915177ad feat(mobile): add search to launch bar 2026-02-04 21:36:00 +02:00
Elian Doran
5adee3e217 fix(mobile/search): missing rounded corners for bulk actions 2026-02-04 21:31:23 +02:00
Elian Doran
278d82645e feat(mobile): use safe area for bottom-aligned menus 2026-02-04 21:15:08 +02:00
Elian Doran
e66f13b471 feat(mobile/search): use bottom menus for large dropdowns 2026-02-04 21:11:43 +02:00
Elian Doran
0e2955b57e feat(mobile/search): group search buttons into split 2026-02-04 21:06:20 +02:00
Elian Doran
2a4d5ec1ec feat(mobile/search): group search options in dropdown 2026-02-04 20:55:55 +02:00
Elian Doran
07ce63de69 chore(mobile/search): remove redundant margin 2026-02-04 20:44:15 +02:00
Elian Doran
b539862eef fix(mobile/search): duplicate search parameters 2026-02-04 20:41:36 +02:00
Elian Doran
ac4be3f8a8 feat(mobile/global_menu): add option to search notes 2026-02-04 20:40:31 +02:00
Elian Doran
0dc2d07b58 fix(mobile): virtual keyboard detection not working on iOS 2026-02-04 19:09:18 +02:00
Elian Doran
b097f9dc21 fix(mobile): fixed tree overflowing container 2026-02-04 18:53:32 +02:00
Elian Doran
8aa4a97480 fix(mobile): fixed tree for launcher duplicated in split 2026-02-04 18:52:46 +02:00
Elian Doran
3e54d0ceae chore(hidden_tree): add icon to mobile tab switcher launcher 2026-02-04 18:48:37 +02:00
Elian Doran
e1cec3404a Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2
; Conflicts:
;	apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx
2026-02-04 18:46:38 +02:00
Elian Doran
9a2b7fbda1 Feature/mobile improvement bugs (#8614) 2026-02-04 18:45:35 +02:00
Elian Doran
e0b4ebed93 chore(mobile): address requested changes 2026-02-04 18:40:30 +02:00
Elian Doran
e00e3999c5 feat(mobile/note_actions): indicate current content language 2026-02-04 18:05:37 +02:00
Elian Doran
2cbe96d815 feat(mobile/note_actions): integrate text content language switcher 2026-02-04 18:03:17 +02:00
Elian Doran
ac3b289c9e feat(mobile): more stable fixed tree for launch bar config 2026-02-04 16:46:22 +02:00
Elian Doran
62534e0e93 chore(mobile/tab_switcher): launcher preview not looking good 2026-02-04 16:43:46 +02:00
Elian Doran
b802c3174c style(mobile): add top border to bottom bar 2026-02-04 16:29:51 +02:00
Elian Doran
aee1a6e1f0 fix(note_list): regression in the display of no previews 2026-02-04 16:28:07 +02:00
Elian Doran
46f1cd38e0 feat(mobile): enforce backdrop effects off 2026-02-04 16:24:14 +02:00
Elian Doran
7f891ef523 feat(mobile/tab_switcher): improve display for some text elements 2026-02-04 15:44:17 +02:00
Elian Doran
06af5e15cd feat(mobile/tab_switcher): consider view scope for preview 2026-02-04 15:41:15 +02:00
Elian Doran
af89a0a883 fix(new_layout): note title actions shown in non-standard view modes 2026-02-04 15:17:39 +02:00
Elian Doran
fa72eb2edb fix(mobile/tab_switcher): view mode not displayed in title 2026-02-04 14:53:00 +02:00
Elian Doran
c1ea94423b refactor(client): use CSS file for content renderer 2026-02-04 14:46:52 +02:00
Elian Doran
1e70d066bd fix(mobile/tab_switcher): wrong preview for relation map 2026-02-04 14:39:58 +02:00
Elian Doran
ab89f16e7c fix(mobile): unnecessary separator for custom note actions 2026-02-04 14:29:47 +02:00
Elian Doran
8c848a4cb5 fix(mobile): wrong context activation logic when creating new split 2026-02-04 14:04:47 +02:00
Elian Doran
e021a54d2d fix(mobile): formatting toolbar not appearing after read-only (closes #5368) 2026-02-04 13:42:30 +02:00
Elian Doran
70523574b0 refactor(client): extract mobile layout CSS to dedicated file 2026-02-04 13:37:04 +02:00
Elian Doran
66e0f1ab19 chore(mobile/tree): slightly bigger expanders 2026-02-04 13:29:45 +02:00
Elian Doran
671a05470e fix(mobile): missing badge style in tree 2026-02-04 13:24:08 +02:00
Elian Doran
99eec0c41e fix(mobile): duplicate promoted attributes 2026-02-04 13:18:26 +02:00
Elian Doran
48d06dcb06 fix(mobile): note context menu too tall in browser 2026-02-04 13:17:28 +02:00
Elian Doran
e38df0c731 fix(mobile): note paths dialog doesn't trigger clone note to location 2026-02-04 13:06:49 +02:00
Elian Doran
0f3f49915e fix(mobile): note actions should be at the bottom, not above launch bar 2026-02-04 12:24:56 +02:00
Elian Doran
416144265b fix(client): wrong positioning of modals due to mobile changes 2026-02-04 12:06:29 +02:00
renovate[bot]
2e7ced8e60 Update dependency @types/node to v24.10.10 2026-02-04 01:52:42 +00:00
renovate[bot]
563463782c Update dependency @ckeditor/ckeditor5-dev-build-tools to v54.3.3 2026-02-04 01:51:41 +00:00
Adorian Doran
fe02871e91 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-02-04 00:45:03 +02:00
Adorian Doran
bb05aeeaf7 style/split: tweak transition for the current split indicator 2026-02-04 00:44:54 +02:00
Elian Doran
846358ccb0 Update dependency @redocly/cli to v2.15.1 (#8601) 2026-02-03 23:45:18 +02:00
Elian Doran
dabc779727 Update dependency @smithy/middleware-retry to v4.4.30 (#8602) 2026-02-03 23:45:02 +02:00
Elian Doran
08fd2ec64b Update dependency happy-dom to v20.5.0 (#8603) 2026-02-03 23:44:32 +02:00
Adorian Doran
c42c06d048 style/global menu: use proper heading for the development options section 2026-02-03 21:01:08 +02:00
Adorian Doran
e951d60800 style/options: hide collection properties 2026-02-03 20:54:56 +02:00
Elian Doran
0d8453f6a7 feat(mobile/note_actions): integrate code language switcher 2026-02-03 18:58:04 +02:00
Elian Doran
52b41b1bb0 feat(mobile/note_actions): integrate similar notes 2026-02-03 18:18:57 +02:00
Elian Doran
c7265017b3 feat(mobile/note_actions): integrate styling for note info 2026-02-03 18:09:02 +02:00
Elian Doran
634e0b6d30 feat(mobile/note_actions): integrate note info 2026-02-03 18:04:08 +02:00
renovate[bot]
1c260f5890 Update dependency happy-dom to v20.5.0 2026-02-03 01:34:19 +00:00
renovate[bot]
177aedeaae Update dependency @smithy/middleware-retry to v4.4.30 2026-02-03 01:33:28 +00:00
renovate[bot]
3f6f3d2565 Update dependency @redocly/cli to v2.15.1 2026-02-03 01:32:42 +00:00
Adorian Doran
38489dbfeb style/collapsible widget: fade content when expanding or collapsing 2026-02-03 03:03:50 +02:00
Adorian Doran
ff2a3d6a28 client/search: fix menu dropdowns getting clipped 2026-02-03 02:56:46 +02:00
Adorian Doran
2c74697fb1 style/text editor: tweak the content placeholder 2026-02-03 02:22:04 +02:00
Adorian Doran
110e9200b9 style/scrolling container: improve alignment when content centering is turned on, refactor 2026-02-03 02:11:57 +02:00
Adorian Doran
6e792f9735 style/tree/tree item icons: fix color for tinted tree items 2026-02-03 01:25:28 +02:00
Adorian Doran
51d8b13a81 style/tree: tweak the color of tree item icons for the dark color scheme 2026-02-03 01:19:47 +02:00
Adorian Doran
a05691fd07 style/tree: add a CSS variable to customize the color of tree item icons 2026-02-03 00:52:36 +02:00
Adorian Doran
d25e7915d9 style/tree/context menu: use the proper color for menu item icons 2026-02-03 00:41:41 +02:00
Elian Doran
1a025dfef3 Translations update from Hosted Weblate (#8581) 2026-02-02 22:09:13 +02:00
ibs-allaow
84cc4194aa Translated using Weblate (Arabic)
Currently translated at 61.1% (93 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ar/
2026-02-02 21:07:55 +01:00
ibs-allaow
331a56277c Translated using Weblate (Arabic)
Currently translated at 59.7% (1055 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-02-02 21:07:54 +01:00
ibs-allaow
bc2915adb9 Translated using Weblate (Arabic)
Currently translated at 42.2% (49 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/ar/
2026-02-02 21:07:53 +01:00
Elian Doran
703fe9a71b Translated using Weblate (Romanian)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ro/
2026-02-02 21:07:52 +01:00
Elian Doran
6c50664046 Translated using Weblate (Romanian)
Currently translated at 99.9% (1766 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ro/
2026-02-02 21:07:51 +01:00
Aindriú Mac Giolla Eoin
673cbc97e1 Translated using Weblate (Irish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-02-02 21:07:49 +01:00
Aindriú Mac Giolla Eoin
3e83766099 Translated using Weblate (Irish)
Currently translated at 0.1% (1 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-02-02 21:07:48 +01:00
Aindriú Mac Giolla Eoin
b453589077 Translated using Weblate (Irish)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ga/
2026-02-02 21:07:47 +01:00
Hosted Weblate
654fa18ab1 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/
2026-02-02 21:07:45 +01:00
Elian Doran
7b0d91534c Add subtreeHidden and map:* attributes to labels documentation (#8594) 2026-02-02 22:07:34 +02:00
Elian Doran
8d4801bb6f Mobile: integrate new layout v1 (#8595) 2026-02-02 21:47:34 +02:00
Elian Doran
9e36f4f625 Merge branch 'main' into feature/mobile_improvements 2026-02-02 21:44:22 +02:00
Elian Doran
d83a824812 chore(client): address requested changes 2026-02-02 21:43:27 +02:00
Elian Doran
ca128f2fa9 chore(client): address requested changes 2026-02-02 21:19:35 +02:00
Elian Doran
c02642d0f9 feat(mobile/note_actions): display backlinks & note paths on same row 2026-02-02 21:16:02 +02:00
Adorian Doran
7340709111 style/quick edit: refactor 2026-02-02 21:13:16 +02:00
hulmgulm
56d0383372 Merge branch 'main' into main 2026-02-02 20:10:42 +01:00
Adorian Doran
49d33ea19a style/quick edit: allow object selection rectangle to go outside of the editor area 2026-02-02 21:10:00 +02:00
hulmgulm
979fa0359a updated texts 2026-02-02 20:08:55 +01:00
Elian Doran
c7381d058a feat(mobile/note_actions): proper styling of note paths 2026-02-02 21:05:25 +02:00
Adorian Doran
5db298f031 style/quick edit: increase the horizontal padding of the text to make space for the block toolbar button 2026-02-02 21:03:02 +02:00
Elian Doran
171d948a00 feat(mobile/note_actions): basic integration of note paths 2026-02-02 21:00:05 +02:00
Adorian Doran
facd56cdf4 style/text editor/block toolbar button: tweak 2026-02-02 20:57:38 +02:00
Elian Doran
ba17ec4be4 chore(backlinks): show multiple excerpts properly 2026-02-02 20:44:48 +02:00
Elian Doran
6c163b5479 chore(mobile/note_actions): flickerless backlinks item 2026-02-02 20:39:37 +02:00
Elian Doran
79649805b8 chore(mobile/note_actions): missing translation for backlinks 2026-02-02 20:34:52 +02:00
Elian Doran
220ca8a570 chore(mobile/note_actions): use new layout styling for backlinks 2026-02-02 20:32:06 +02:00
Adorian Doran
348c00f86d style/text editor: fix layout issues 2026-02-02 20:13:00 +02:00
Elian Doran
12b641b522 feat(mobile/note_actions): basic integration of backlinks 2026-02-02 20:10:20 +02:00
hulmgulm
6855bc1de6 Merge branch 'TriliumNext:main' into main 2026-02-02 19:09:08 +01:00
Adorian Doran
a9c5b99ae8 style/text editor/block toolbar button: fix position and z-index 2026-02-02 20:06:05 +02:00
Elian Doran
76f36e2fd3 fix(mobile/custom_note_actions): unable to close empty pane 2026-02-02 18:39:40 +02:00
Adorian Doran
afe710321c style/text editor: fix layout issues 2026-02-02 18:34:22 +02:00
Elian Doran
0d444daaca fix(mobile/custom_note_actions): note icon shown in empty note 2026-02-02 18:29:21 +02:00
Elian Doran
c8a0c9fd23 chore(mobile/custom_note_actions): text not fitting 2026-02-02 18:26:40 +02:00
Elian Doran
79f07ae923 chore(mobile/custom_note_actions): disable split orientation button 2026-02-02 18:23:54 +02:00
Elian Doran
c77e7a568b chore(mobile/custom_note_actions): duplicate open help 2026-02-02 18:11:58 +02:00
Elian Doran
ff9ec2057b chore(mobile/custom_note_actions): hide open note externally 2026-02-02 18:10:26 +02:00
Elian Doran
6a313b99e4 fix(mobile/custom_note_actions): missing file upload 2026-02-02 18:02:47 +02:00
Elian Doran
8a92370042 fix(mobile/custom_note_actions): missing separator 2026-02-02 17:57:30 +02:00
Elian Doran
411a59ec54 feat(mobile): display custom note actions in note actions 2026-02-02 17:46:18 +02:00
Elian Doran
2d4022044d chore(mobile): get rid of floating buttons 2026-02-02 17:30:35 +02:00
Elian Doran
bbc5ebd76b chore(mobile/header): improve button sizes 2026-02-02 17:06:27 +02:00
Elian Doran
e9c90fcde8 chore(mobile/header): prevent badges from shrinking 2026-02-02 17:02:32 +02:00
Elian Doran
911f78867f chore(mobile/header): make icons easier to press 2026-02-02 16:58:06 +02:00
Elian Doran
5507cc5abc chore(mobile): slightly smaller note title icon 2026-02-02 16:51:57 +02:00
Elian Doran
0e5aa401ef chore(mobile/note_icon): redundant separator 2026-02-02 16:44:08 +02:00
Elian Doran
d48473ab87 feat(mobile/note_icon): single menu for filtering & resetting 2026-02-02 16:38:12 +02:00
hulmgulm
734efaf40c Update apps/server/src/assets/doc_notes/en/User Guide/User Guide/Advanced Usage/Attributes/Labels.html
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-02-02 14:41:38 +01:00
hulmgulm
f89718d88a Add subtreeHidden and map:* attributes to Labels.html 2026-02-02 14:38:11 +01:00
Elian Doran
aa4942a0da fix(mobile/note_icon): wrong icons displayed 2026-02-02 13:45:13 +02:00
Elian Doran
90bb162a88 feat(mobile/note_icon): bigger touch area 2026-02-02 13:31:29 +02:00
Elian Doran
0382a4b30e fix(mobile/note_icon): consistent height and proper margins 2026-02-02 13:27:34 +02:00
Elian Doran
490d940cd1 feat(mobile/note_icon): improve height and fit 2026-02-02 13:23:55 +02:00
Elian Doran
b090eb9359 feat(mobile/note_icon): calculate number of columns dynamically 2026-02-02 13:22:37 +02:00
Elian Doran
cb9e67ce84 fix(mobile/note_icon): small horizontal scroll 2026-02-02 13:14:42 +02:00
Elian Doran
c4d131dd23 feat(mobile): improve note icon selector fit 2026-02-02 13:13:13 +02:00
Elian Doran
2667f266bf feat(mobile): use modal for icon selector 2026-02-02 13:05:20 +02:00
Elian Doran
8258936d6c feat(mobile): integrate icon display in header 2026-02-02 12:53:54 +02:00
Elian Doran
e4e7449078 feat(mobile): integrate inline title with title actions 2026-02-02 12:51:29 +02:00
Elian Doran
11b020e859 feat(mobile): integrate part of the new layout title row 2026-02-02 12:47:43 +02:00
Elian Doran
72d6b83ec5 docs(user): add nightly release script for Windows 2026-02-02 12:20:38 +02:00
Elian Doran
fbe5152cb3 chore(deps): update dependency webdriverio to v9.23.3 (#8583) 2026-02-02 07:48:30 +02:00
Elian Doran
6bef01f755 fix(deps): update dependency globals to v17.3.0 (#8584) 2026-02-02 07:48:17 +02:00
Elian Doran
f0d4b4a6d9 fix(deps): update dependency mind-elixir to v5.7.1 (#8585) 2026-02-02 07:48:05 +02:00
renovate[bot]
a54fe62643 fix(deps): update dependency mind-elixir to v5.7.1 2026-02-02 04:31:50 +00:00
renovate[bot]
eb4bbd49fb fix(deps): update dependency globals to v17.3.0 2026-02-02 01:52:08 +00:00
renovate[bot]
ab4f1bd4f4 chore(deps): update dependency webdriverio to v9.23.3 2026-02-02 01:51:29 +00:00
Elian Doran
f8b414c354 feat(etapi): add attachments etapi endpoint (#8578) 2026-02-01 22:34:37 +02:00
Elian Doran
82a21624c3 Translations update from Hosted Weblate (#8579) 2026-02-01 22:32:35 +02:00
Elian Doran
1b212ac720 Improved mobile note actions (#8580) 2026-02-01 22:30:45 +02:00
Elian Doran
c36ce3ea14 fix(mobile/note_actions): insert child note not working 2026-02-01 22:29:43 +02:00
Elian Doran
841fab77a8 chore(client): address requested changes 2026-02-01 22:22:25 +02:00
Elian Doran
fd6f910824 fix(mobile/note_actions): backdrop remains when closing split 2026-02-01 22:02:54 +02:00
Elian Doran
ce9ca1917d fix(mobile/note_actions): reintroduce split buttons 2026-02-01 22:01:26 +02:00
perfectra1n
c702fb273c feat(tests): add tests for new attachments endpoint 2026-02-01 11:46:03 -08:00
Elian Doran
4c72d8691b fix(mobile/note_actions): reintroduce help button 2026-02-01 21:45:43 +02:00
Elian Doran
35ac5fc514 fix(mobile/note_actions): reintroduce insert child note 2026-02-01 21:42:48 +02:00
Elian Doran
92991cc03c fix(promoted_attributes): displayed in non-default view modes 2026-02-01 21:37:20 +02:00
Elian Doran
76492475e3 fix(mobile/note_actions): find not working 2026-02-01 21:27:24 +02:00
Elian Doran
e2363d860c fix(mobile/note_actions): font too big 2026-02-01 21:25:40 +02:00
Elian Doran
c06a90913a fix(mobile/note_actions): submenus not working 2026-02-01 21:12:50 +02:00
Elian Doran
e88c0f7326 fix(mobile/note_actions): toggles on two rows 2026-02-01 21:02:46 +02:00
Elian Doran
2a4280b5bf feat(mobile/note_actions): remove bottom rounded corners 2026-02-01 20:59:50 +02:00
Elian Doran
d3c733c57f feat(mobile/note_actions): add backdrop 2026-02-01 20:57:01 +02:00
Elian Doran
f91add3cd4 chore(mobile/note_actions): position like global menu 2026-02-01 20:53:33 +02:00
Elian Doran
90e3f7508a chore(mobile): enforce new layout 2026-02-01 20:50:00 +02:00
Elian Doran
5e981da4df feat(mobile): use the desktop version of note actions 2026-02-01 20:48:55 +02:00
Aindriú Mac Giolla Eoin
2ddd5d75fc Added translation using Weblate (Irish) 2026-02-01 19:33:10 +01:00
Aindriú Mac Giolla Eoin
88f509cbb6 Added translation using Weblate (Irish) 2026-02-01 19:33:08 +01:00
Aindriú Mac Giolla Eoin
ca161bc881 Added translation using Weblate (Irish) 2026-02-01 19:33:05 +01:00
Aindriú Mac Giolla Eoin
676ca44cdf Added translation using Weblate (Irish) 2026-02-01 19:33:01 +01:00
green
5d0c91202f Translated using Weblate (Japanese)
Currently translated at 100.0% (1767 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-02-01 19:32:59 +01:00
Ulices
a166f049d5 Translated using Weblate (Spanish)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/es/
2026-02-01 19:32:59 +01:00
noobhjy
0dc4692dfc Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1767 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2026-02-01 19:32:59 +01:00
ibs-allaow
996607d096 Translated using Weblate (Arabic)
Currently translated at 59.5% (1053 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ar/
2026-02-01 19:32:58 +01:00
Ulices
bd907ea008 Translated using Weblate (Spanish)
Currently translated at 100.0% (1767 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/es/
2026-02-01 19:32:58 +01:00
noobhjy
b1573b1f3b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hans/
2026-02-01 19:32:57 +01:00
Marcel
246849ce94 Translated using Weblate (German)
Currently translated at 100.0% (1767 of 1767 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/de/
2026-02-01 19:32:57 +01:00
Marcel
2c2c68261a Translated using Weblate (German)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/de/
2026-02-01 19:32:56 +01:00
green
e38a0361bf Translated using Weblate (Japanese)
Currently translated at 100.0% (389 of 389 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ja/
2026-02-01 19:32:56 +01:00
ibs-allaow
cbf879bd32 Translated using Weblate (Arabic)
Currently translated at 59.2% (90 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ar/
2026-02-01 19:32:56 +01:00
perfectra1n
808625e564 feat(etapi): add attachments etapi endpoint 2026-02-01 09:19:37 -08:00
Elian Doran
c3b4c2f7d4 fix(desktop): background effects interferes with native title bar 2026-02-01 15:47:07 +02:00
Elian Doran
14f521fdd7 Revert "chore(ci): try to fix flaky "Merge manifest lists""
This reverts commit eea4cbbd6c.
2026-02-01 11:53:57 +02:00
Elian Doran
087831df5a feat(etapi): add revisions route and "undelete" route to etapi (#8455) 2026-02-01 11:45:20 +02:00
Elian Doran
6b0542a5bf fix(deps): update dependency preact to v10.28.3 (#8570) 2026-02-01 10:11:48 +02:00
Elian Doran
ac57856f00 fix(deps): update dependency react-window to v2.2.6 (#8571) 2026-02-01 10:10:46 +02:00
renovate[bot]
2ab1587df0 fix(deps): update dependency react-window to v2.2.6 2026-02-01 00:33:14 +00:00
renovate[bot]
632aa6e003 fix(deps): update dependency preact to v10.28.3 2026-02-01 00:32:20 +00:00
perfectra1n
280697f2f7 Revert "feat(etapi): resolve suggestions for norms from gemini"
This reverts commit 0650be664d.
2026-01-21 16:37:02 -08:00
perfectra1n
0650be664d feat(etapi): resolve suggestions for norms from gemini 2026-01-21 16:33:42 -08:00
perfectra1n
60c61f553a feat(etapi): put filtering for revisions mainly in the db layer 2026-01-21 16:30:37 -08:00
perfectra1n
022c967781 feat(etapi): add revisions route and "undelete" route to etapi 2026-01-21 16:25:17 -08:00
290 changed files with 12519 additions and 4468 deletions

View File

@@ -271,16 +271,13 @@ jobs:
REF_NAME=$(echo "${GITHUB_REF}" | sed 's/refs\/heads\///' | sed 's/refs\/tags\///')
# Create and push the manifest list with both the branch/tag name and the commit SHA
# Note: Images are only pushed to GHCR during build, so we always reference GHCR sources
# and copy to DockerHub using imagetools create
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Copy from GHCR to DockerHub (source digests only exist on GHCR)
docker buildx imagetools create \
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
@@ -290,10 +287,9 @@ jobs:
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Copy stable tag from GHCR to DockerHub
docker buildx imagetools create \
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Small delay to ensure stable tag is fully propagated
sleep 5
@@ -305,7 +301,7 @@ jobs:
docker buildx imagetools create \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
fi

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.13.1

View File

@@ -42,5 +42,8 @@
},
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
],
"cSpell.words": [
"Trilium"
]
}
}

View File

@@ -9,14 +9,14 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.28.2",
"packageManager": "pnpm@10.29.3",
"devDependencies": {
"@redocly/cli": "2.15.0",
"@redocly/cli": "2.18.1",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.16",
"typedoc": "0.28.17",
"typedoc-plugin-missing-exports": "4.1.2"
}
}

View File

@@ -23,11 +23,10 @@
"@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.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.6.2",
"@preact/signals": "2.8.0",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
@@ -42,26 +41,25 @@
"color": "5.0.3",
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "17.2.0",
"i18next": "25.8.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"i18next": "25.8.7",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
"jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6",
"katex": "0.16.28",
"knockout": "3.5.1",
"leaflet": "1.9.4",
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.1",
"maplibre-gl": "5.18.0",
"marked": "17.0.2",
"mermaid": "11.12.2",
"mind-elixir": "5.6.1",
"mind-elixir": "5.8.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.2",
"preact": "10.28.3",
"react-i18next": "16.5.4",
"react-window": "2.2.5",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -72,13 +70,11 @@
"@prefresh/vite": "2.4.11",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@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": "13.0.1",
"happy-dom": "20.4.0",
"happy-dom": "20.6.1",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"

View File

@@ -46,10 +46,6 @@ if (utils.isElectron()) {
electronContextMenu.setupContextMenu();
}
if (utils.isPWA()) {
initPWATopbarColor();
}
function initOnElectron() {
const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
@@ -134,20 +130,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
nativeTheme.themeSource = themeSource;
}
function initPWATopbarColor() {
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}

View File

@@ -700,6 +700,15 @@ export default class FNote {
return this.hasAttribute(LABEL, name);
}
/**
* Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import).
* @param name the name of the label to look for.
* @returns `true` if the label exists, or its version with the `disabled:` prefix.
*/
hasLabelOrDisabled(name: string) {
return this.hasLabel(name) || this.hasLabel(`disabled:${name}`);
}
/**
* @param name - label name
* @returns true if label exists (including inherited) and does not have "false" value.

View File

@@ -0,0 +1,76 @@
#background-color-tracker {
color: var(--main-background-color) !important;
}
span.keyboard-shortcut,
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
/* #region Tree */
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-inline-start: 10px;
}
.fancytree-title {
margin-inline-start: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
span.fancytree-expander {
width: 24px !important;
margin-inline-end: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-inline-end: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
/* #endregion */

View File

@@ -1,128 +1,40 @@
import "./mobile_layout.css";
import type AppContext from "../components/app_context.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import FindWidget from "../widgets/find.js";
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 MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import TabRowWidget from "../widgets/tab_row.js";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import { applyModals } from "./layout_commons.js";
const MOBILE_CSS = `
<style>
span.keyboard-shortcut,
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
</style>`;
const FANCYTREE_CSS = `
<style>
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-inline-start: 10px;
}
.fancytree-custom-icon {
font-size: 2em;
}
.fancytree-title {
font-size: 1.5em;
margin-inline-start: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
.fancytree-node .fancytree-expander:before {
font-size: 2em !important;
}
span.fancytree-expander {
width: 24px !important;
margin-inline-end: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-inline-end: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
</style>`;
export default class MobileLayout {
getRootWidget(appContext: typeof AppContext) {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(
new FlexContainer("row")
@@ -136,7 +48,7 @@ export default class MobileLayout {
.css("padding-inline-start", "0")
.css("padding-inline-end", "0")
.css("contain", "content")
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
)
.child(
new ScreenContainer("detail", "row")
@@ -147,30 +59,28 @@ export default class MobileLayout {
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.class("title-row note-split-title")
.contentSized()
.css("font-size", "larger")
.css("align-items", "center")
.child(<ToggleSidebarButton />)
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(<NoteBadges />)
.child(<MobileDetailMenu />)
)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(<PromotedAttributes />)
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfoWidget />)
)
.child(<InlineTitle />)
.child(<NoteTitleActions />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
.child(<ScrollPadding />)
)
.child(<MobileEditorToolbar />)
.child(new FindWidget())
)
)
)
@@ -191,13 +101,3 @@ export default class MobileLayout {
return rootContainer;
}
}
function FilePropertiesWrapper() {
const { note, ntxId } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
</div>
);
}

View File

@@ -248,7 +248,7 @@ class ContextMenu {
if ("uiIcon" in item || "checked" in item) {
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
if (icon) {
$icon.addClass(icon);
$icon.addClass([icon, "tn-icon"]);
} else {
$icon.append("&nbsp;");
}

View File

@@ -1,12 +1,12 @@
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from "../services/i18n.js";
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import treeService from "../services/tree.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
@@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const parentNoteId = this.node.getParent().data.noteId;
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem;

View File

@@ -1,4 +1,4 @@
import type { LeafletMouseEvent } from "leaflet";
import type { GeoMouseEvent } from "../widgets/collections/geomap/map.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import { t } from "../services/i18n.js";
@@ -16,7 +16,7 @@ function openContextMenu(notePath: string, e: ContextMenuEvent, viewScope: ViewS
});
}
function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandNames>[] {
function getItems(e: ContextMenuEvent | GeoMouseEvent): MenuItem<CommandNames>[] {
const ntxId = getNtxId(e);
const isMobileSplitOpen = isMobile() && appContext.tabManager.getNoteContextById(ntxId).getMainContext().getSubContexts().length > 1;
@@ -28,7 +28,7 @@ function getItems(e: ContextMenuEvent | LeafletMouseEvent): MenuItem<CommandName
];
}
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | LeafletMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEvent | GeoMouseEvent, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
}
@@ -52,7 +52,7 @@ function handleLinkContextMenuItem(command: string | undefined, e: ContextMenuEv
return false;
}
function getNtxId(e: ContextMenuEvent | LeafletMouseEvent) {
function getNtxId(e: ContextMenuEvent | GeoMouseEvent) {
if (utils.isDesktop()) {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
if (!subContexts) return null;

View File

@@ -18,6 +18,10 @@ export type PrintReport = {
} | {
type: "collection";
ignoredNoteIds: string[];
} | {
type: "error";
message: string;
stack?: string;
};
async function main() {

View File

@@ -168,6 +168,49 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin
return false;
}
/**
* Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`.
*
* Note that this work for non-dangerous attributes as well.
*
* If there are multiple attributes with the same name, all of them will be toggled at the same time.
*
* @param note the note whose attribute to change.
* @param type the type of dangerous attribute (label or relation).
* @param name the name of the dangerous attribute.
* @param willEnable whether to enable or disable the attribute.
* @returns a promise that will resolve when the request to the server completes.
*/
async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) {
const attrs = [
...note.getOwnedAttributes(type, name),
...note.getOwnedAttributes(type, `disabled:${name}`)
];
for (const attr of attrs) {
const baseName = getNameWithoutDangerousPrefix(attr.name);
const newName = willEnable ? baseName : `disabled:${baseName}`;
if (newName === attr.name) continue;
// We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically.
if (attr.type === "label") {
await setLabel(note.noteId, newName, attr.value);
} else {
await setRelation(note.noteId, newName, attr.value);
}
await removeAttributeById(note.noteId, attr.attributeId);
}
}
/**
* Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled.
* @param name the name of an attribute.
* @returns the name without the `disabled:` prefix.
*/
function getNameWithoutDangerousPrefix(name: string) {
return name.startsWith("disabled:") ? name.substring(9) : name;
}
export default {
addLabel,
setLabel,
@@ -177,5 +220,7 @@ export default {
removeAttributeById,
removeOwnedLabelByName,
removeOwnedRelationByName,
isAffecting
isAffecting,
toggleDangerousAttribute,
getNameWithoutDangerousPrefix
};

View File

@@ -0,0 +1,9 @@
.rendered-content.no-preview > div {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 100%;
font-size: 500%;
flex-grow: 1;
}

View File

@@ -1,3 +1,5 @@
import "./content_renderer.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import WheelZoom from 'vanilla-js-wheel-zoom';
@@ -71,18 +73,9 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
} else if (entity instanceof FNote) {
$renderedContent
.css("display", "flex")
.css("flex-direction", "column");
$renderedContent.addClass("no-preview");
$renderedContent.append(
$("<div>")
.css("display", "flex")
.css("justify-content", "space-around")
.css("align-items", "center")
.css("height", "100%")
.css("font-size", "500%")
.css("flex-grow", "1")
.append($("<span>").addClass(entity.getIcon()))
$("<div>").append($("<span>").addClass(entity.getIcon()))
);
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
@@ -292,10 +285,11 @@ function getRenderingType(entity: FNote | FAttachment) {
}
const mime = "mime" in entity && entity.mime;
const isIconPack = entity instanceof FNote && entity.hasLabel("iconPack");
if (type === "file" && mime === "application/pdf") {
type = "pdf";
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime) && !isIconPack) {
type = "code";
} else if (type === "file" && mime && mime.startsWith("audio/")) {
type = "audio";

View File

@@ -1,5 +1,6 @@
import { t } from "./i18n";
import options from "./options";
import { isMobile } from "./utils";
export interface ExperimentalFeature {
id: string;
@@ -21,7 +22,7 @@ let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
if (featureId === "new-layout") {
return options.is("newLayout");
return (isMobile() || options.is("newLayout"));
}
return getEnabledFeatures().has(featureId);
@@ -29,7 +30,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
export function getEnabledExperimentalFeatureIds() {
const values = [ ...getEnabledFeatures().values() ];
if (options.is("newLayout")) {
if (isMobile() || options.is("newLayout")) {
values.push("new-layout");
}
return values;

View File

@@ -6,7 +6,7 @@ import bundleService, { type Bundle } from "./bundle.js";
import froca from "./froca.js";
import server from "./server.js";
async function render(note: FNote, $el: JQuery<HTMLElement>) {
async function render(note: FNote, $el: JQuery<HTMLElement>, onError?: (e: unknown) => void) {
const relations = note.getRelations("renderNote");
const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId);
@@ -21,12 +21,14 @@ async function render(note: FNote, $el: JQuery<HTMLElement>) {
$scriptContainer.append(bundle.html);
// async so that scripts cannot block trilium execution
bundleService.executeBundle(bundle, note, $scriptContainer).then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el);
}
});
bundleService.executeBundle(bundle, note, $scriptContainer)
.catch(onError)
.then(result => {
// Render JSX
if (bundle.html === "") {
renderIfJsx(bundle, result, $el).catch(onError);
}
});
}
return renderNoteIds.length > 0;

View File

@@ -28,7 +28,7 @@
--bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
--tn-modal-max-height: 90vh;
--tn-modal-max-height: 90svh;
--tree-item-light-theme-max-color-lightness: 50;
--tree-item-dark-theme-min-color-lightness: 75;
@@ -111,6 +111,7 @@ body.mobile #root-widget.virtual-keyboard-opened #mobile-bottom-bar {
}
#mobile-bottom-bar {
border-top: 1px solid var(--main-border-color);
padding-bottom: var(--mobile-bottom-offset);
}
@@ -152,6 +153,11 @@ textarea,
background: var(--input-background-color);
}
.form-control:disabled {
background-color: var(--input-background-color);
opacity: 0.6;
}
.form-control:focus {
color: var(--input-text-color);
background: var(--input-background-color);
@@ -409,6 +415,7 @@ body.desktop .tabulator-popup-container,
.dropdown-menu.static {
box-shadow: unset;
backdrop-filter: unset !important;
}
.dropend .dropdown-toggle::after {
@@ -454,7 +461,7 @@ body.desktop .tabulator-popup-container,
visibility: hidden;
}
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
.dropdown-menu:not(#context-menu-container) .dropdown-item,
body.desktop .dropdown-menu .dropdown-toggle,
body #context-menu-container .dropdown-item > span,
body.mobile .dropdown .dropdown-submenu > span {
@@ -462,6 +469,15 @@ body.mobile .dropdown .dropdown-submenu > span {
align-items: center;
}
body.mobile .dropdown .dropdown-submenu {
flex-wrap: wrap;
& > span {
flex-grow: 1;
}
}
.dropdown-item span.keyboard-shortcut,
.dropdown-item *:not(.keyboard-shortcut) > kbd {
flex-grow: 1;
@@ -931,6 +947,7 @@ table.promoted-attributes-in-tooltip th {
color: var(--muted-text-color);
opacity: 0.6;
line-height: 1;
word-wrap: break-word;
}
.aa-dropdown-menu .aa-suggestion p {
@@ -1325,15 +1342,12 @@ body.desktop .dropdown-submenu > .dropdown-menu {
max-width: 300px;
}
.dropdown-submenu.dropstart > .dropdown-menu {
.dropdown-submenu.dropstart > .dropdown-menu,
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: auto;
inset-inline-end: calc(100% - 2px);
}
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-start: calc(-100% + 10px);
}
.right-dropdown-widget {
flex-shrink: 0;
}
@@ -1530,7 +1544,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
@media (max-width: 991px) {
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
body.mobile #launcher-container .dropdown > .dropdown-menu.show,
body.mobile .dropdown-menu.mobile-bottom-menu.show {
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
position: fixed !important;
bottom: var(--dropdown-bottom) !important;
@@ -1542,6 +1557,16 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
}
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
body.mobile .dropdown-menu.mobile-bottom-menu.show {
--dropdown-bottom: 0px;
padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important;
}
#mobile-sidebar-container {
position: fixed;
top: 0;
@@ -1667,47 +1692,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background: var(--main-background-color);
}
.modal-dialog {
margin: var(--bs-modal-margin);
max-width: 80%;
}
body.mobile {
.modal-dialog {
margin: var(--bs-modal-margin);
max-width: 80%;
}
.modal-content {
height: 100%;
}
}
@media (max-width: 991px) {
body.mobile.force-fixed-tree #mobile-sidebar-wrapper {
padding-top: 0;
position: static;
height: 40vh;
width: 100vw;
transform: none !important;
background-color: var(--left-pane-background-color) !important;
border-bottom: 0.5px solid var(--main-border-color);
}
body.mobile.force-fixed-tree #mobile-sidebar-container {
display: none !important;
}
body.mobile.force-fixed-tree #mobile-sidebar-wrapper .quick-search {
display: none;
}
body.mobile.force-fixed-tree .component > button.bx-sidebar {
visibility: hidden;
padding: 0;
width: 6px;
}
body.mobile.force-fixed-tree #mobile-rest-container {
flex-direction: column !important;
}
body.mobile.force-fixed-tree #detail-container {
flex-grow: 1;
.modal-content {
height: 100%;
}
}
}
@@ -2623,14 +2616,14 @@ iframe.print-iframe {
}
}
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
#root-widget.virtual-keyboard-opened .note-split:not(.active) {
max-height: 80px;
opacity: 0.4;
}
}
}
body.desktop .title-row {
.title-row {
height: 50px;
min-height: 50px;
align-items: center;

View File

@@ -134,6 +134,7 @@
--left-pane-collapsed-border-color: #0009;
--left-pane-background-color: #1f1f1f;
--left-pane-text-color: #aaaaaa;
--left-pane-icon-color: #c5c5c5;
--left-pane-item-hover-background: #ffffff0d;
--left-pane-item-selected-background: #ffffff25;
--left-pane-item-selected-color: #dfdfdf;
@@ -209,6 +210,7 @@
--badge-share-background-color: #4d4d4d;
--badge-clipped-note-background-color: #295773;
--badge-execute-background-color: #604180;
--badge-active-content-background-color: rgb(12, 68, 70);
--note-icon-background-color: #444444;
--note-icon-color: #d4d4d4;
@@ -237,9 +239,9 @@
--bottom-panel-background-color: #11111180;
--bottom-panel-title-bar-background-color: #3F3F3F80;
--status-bar-border-color: var(--main-border-color);
--scrollbar-thumb-color: #fdfdfd5c;
--scrollbar-thumb-hover-color: #ffffff7d;
--scrollbar-background-color: transparent;
@@ -336,6 +338,17 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
}
.modal.tab-bar-modal .tabs .tab-card.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 16.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
.use-note-color {
--custom-color: var(--dark-theme-custom-color);
}
@@ -350,4 +363,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%);
}
}

View File

@@ -127,6 +127,7 @@
--left-pane-collapsed-border-color: #0000000d;
--left-pane-background-color: #f2f2f2;
--left-pane-text-color: #383838;
--left-pane-icon-color: currentColor;
--left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
--left-pane-item-selected-background: white;
--left-pane-item-selected-color: black;
@@ -201,6 +202,7 @@
--badge-share-background-color: #6b6b6b;
--badge-clipped-note-background-color: #2284c0;
--badge-execute-background-color: #7b47af;
--badge-active-content-background-color: rgb(27, 164, 168);
--note-icon-background-color: #4f4f4f;
--note-icon-color: white;
@@ -311,6 +313,16 @@
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
}
.modal.tab-bar-modal .tabs .tab-card.with-hue {
background-color: hsl(var(--bg-hue), 56%, 96%);
border-color: hsl(var(--bg-hue), 33%, 41%);
}
.modal.tab-bar-modal .tabs .tab-card.active.with-hue {
background-color: hsl(var(--bg-hue), 86%, 96%);
border-color: hsl(var(--bg-hue), 33%, 41%);
}
.note-split.with-hue,
.quick-edit-dialog-wrapper.with-hue {
--note-icon-custom-background-color: hsl(var(--custom-color-hue), 44.5%, 43.1%);
@@ -321,4 +333,4 @@
.note-split.with-hue *::selection,
.quick-edit-dialog-wrapper.with-hue *::selection {
--selection-background-color: hsl(var(--custom-color-hue), 60%, 90%);
}
}

View File

@@ -800,3 +800,18 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
background: var(--hover-item-background-color);
color: var(--hover-item-text-color);
}
/*
* Alert bars
*/
div.alert {
margin-bottom: 8px;
background: var(--alert-bar-background) !important;
border-radius: 8px;
font-size: .85em;
}
div.alert p + p {
margin-block: 1em 0;
}

View File

@@ -84,6 +84,22 @@ button.btn.btn-success kbd {
letter-spacing: 0.5pt;
}
/*
* Low profile buttons
*/
button.tn-low-profile {
appearance: none;
background: transparent;
border: 0;
border-radius: 8px;
color: inherit;
}
button.tn-low-profile:hover {
background-color: var(--icon-button-hover-background);
}
/*
* Icon buttons
*/
@@ -794,3 +810,35 @@ input[type="range"] {
scrollbar-width: unset;
}
}
/*
* Centered forms
*/
.tn-centered-form {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20vh;
}
.tn-centered-form .form-group {
text-align: center;
color: var(--muted-text-color);
}
.tn-centered-form .form-icon {
font-size: 140px;
color: var(--main-border-color);
}
.tn-centered-form .protected-session-password {
margin-inline: auto;
max-width: 350px;
text-align: center;
}
.tn-centered-form .input-group,
.tn-centered-form button {
margin-top: 12px;
}

View File

@@ -47,9 +47,14 @@
}
/* The toolbar show / hide button for the current text block */
.ck.ck-block-toolbar-button {
:root .ck.ck-block-toolbar-button {
--ck-color-block-toolbar-button: var(--muted-text-color);
--ck-color-button-on-background: transparent;
--ck-color-button-on-color: currentColor;
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
translate: -40% 0;
min-width: 0;
padding: 0;
z-index: 1600;
}
:root .ck.ck-toolbar .ck-button:not(.ck-disabled):active,
@@ -517,6 +522,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
* EDITOR'S CONTENT
*/
.note-detail-editable-text-editor > .ck-placeholder {
opacity: .5;
}
/*
* Code Blocks
*/

View File

@@ -57,12 +57,12 @@
height: 18px;
}
/*
/*
* SEARCH PAGE
*/
/* Button bar */
.search-definition-widget .search-setting-table tbody:last-child div {
.search-definition-widget .search-setting-table .search-actions-container {
justify-content: flex-end;
gap: 8px;
}
@@ -143,7 +143,7 @@
/*
* OPTIONS PAGES
*/
:root {
--options-card-min-width: 500px;
--options-card-max-width: 900px;
@@ -156,6 +156,10 @@
--preferred-max-content-width: var(--options-card-max-width);
}
.note-split.options .collection-properties {
visibility: hidden;
}
/* Create a gap at the top of the option pages */
.note-detail-content-widget-content.options>*:first-child {
margin-top: var(--options-first-item-top-margin, 1em);
@@ -261,13 +265,6 @@ body.desktop .options-section:not(.tn-no-card) {
margin-bottom: 6px;
}
.options-section .alert {
margin-bottom: 8px;
background: var(--alert-bar-background) !important;
border-radius: 8px;
font-size: .85em;
}
nav.options-section-tabs {
min-width: var(--options-card-min-width);
max-width: var(--options-card-max-width);
@@ -331,4 +328,4 @@ nav.options-section-tabs + .options-section {
.etapi-options-section div {
height: auto !important;
}
}

View File

@@ -739,18 +739,12 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
transform: translateX(-25%);
}
body.mobile .fancytree-expander::before,
body.mobile .fancytree-title,
body.mobile .fancytree-node > span {
font-size: 1rem !important;
}
@media (max-width: 991px) {
body.mobile #mobile-sidebar-container {
background-color: rgba(0, 0, 0, 0.5);
}
body.mobile:not(.force-fixed-tree) #mobile-sidebar-wrapper {
body.mobile #mobile-sidebar-wrapper {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-inline-end: 1px solid var(--subtle-border-color);
@@ -769,9 +763,9 @@ body.mobile .fancytree-node > span {
#left-pane .fancytree-custom-icon {
margin-top: 0; /* Use this to align the icon with the tree view item's caption */
color: var(--custom-color, var(--left-pane-icon-color));
}
#left-pane span.fancytree-active .fancytree-title {
font-weight: normal;
}
@@ -1272,7 +1266,7 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 250ms ease-in;
transition: border-color 150ms ease-out;
border: 2px solid transparent;
}
@@ -1322,7 +1316,7 @@ body.mobile .note-title {
margin-inline-start: 0;
}
.title-row {
body.desktop .title-row {
/* Aligns the "Create new split" button with the note menu button (the three dots button) */
padding-inline-end: 3px;
}

View File

@@ -206,6 +206,7 @@ span.fancytree-selected .fancytree-title {
}
span.fancytree-selected .fancytree-custom-icon::before {
font-family: "boxicons";
content: "\eb43";
border: 1px solid var(--main-border-color);
border-radius: 3px;

View File

@@ -29,7 +29,9 @@
"widget-render-error": {
"title": "فشل عرض عنصر واجهة مستخدم React مخصص"
},
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك."
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.",
"open-script-note": "فتح ملاحظة برمجية",
"scripting-error": "خطأ في النص البرمجي المخصص: {{title}}"
},
"add_link": {
"add_link": "أضافة رابط",
@@ -37,14 +39,19 @@
"search_note": "البحث عن الملاحظة بالاسم",
"link_title": "عنوان الرابط",
"button_add_link": "اضافة رابط",
"help_on_links": "مساعدة حول الارتباطات التشعبية"
"help_on_links": "مساعدة حول الارتباطات التشعبية",
"link_title_mirrors": "عنوان الرابط يعكس العنوان الحالي للملاحظة",
"link_title_arbitrary": "يمكن تغيير عنوان الرابط حسب الرغبة"
},
"branch_prefix": {
"edit_branch_prefix": "تعديل بادئة الفرع",
"prefix": "البادئة: ",
"save": "حفظ",
"help_on_tree_prefix": "مساعدة حول بادئة الشجرة",
"branch_prefix_saved": "تم حفظ بادئة الفرع."
"branch_prefix_saved": "تم حفظ بادئة الفرع.",
"edit_branch_prefix_multiple": "تعديل البادئة لـ {{count}} من تفرعات الملاحظات",
"branch_prefix_saved_multiple": "تم حفظ بادئة التفرع لـ {{count}} من التفرعات.",
"affected_branches": "الفروع المتأثرة ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "اجراءات جماعية",
@@ -1173,9 +1180,6 @@
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
},
"web_view": {
"web_view": "عرض الويب"
},
"consistency_checks": {
"title": "فحوصات التناسق"
},

View File

@@ -662,7 +662,8 @@
"show-cheatsheet": "显示快捷帮助",
"toggle-zen-mode": "禅模式",
"new-version-available": "新更新可用",
"download-update": "取得版本 {{latestVersion}}"
"download-update": "取得版本 {{latestVersion}}",
"search_notes": "搜索笔记"
},
"zen_mode": {
"button_exit": "退出禅模式"
@@ -745,7 +746,7 @@
"button_title": "导出SVG格式图片"
},
"relation_map_buttons": {
"create_child_note_title": "创建新的子笔记并添加到关系图",
"create_child_note_title": "创建子笔记并添加到图",
"reset_pan_zoom_title": "重置平移和缩放到初始坐标和放大倍率",
"zoom_in_title": "放大",
"zoom_out_title": "缩小"
@@ -759,7 +760,9 @@
"delete_this_note": "删除此笔记",
"error_cannot_get_branch_id": "无法获取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "无法识别的命令 {{command}}",
"note_revisions": "笔记历史版本"
"note_revisions": "笔记历史版本",
"backlinks": "反链",
"content_language_switcher": "内容语言: {{language}}"
},
"note_icon": {
"change_note_icon": "更改笔记图标",
@@ -910,7 +913,8 @@
"unknown_search_option": "未知的搜索选项 {{searchOptionName}}",
"search_note_saved": "搜索笔记已保存到 {{- notePathTitle}}",
"actions_executed": "操作已执行。",
"view_options": "查看选项:"
"view_options": "查看选项:",
"option": "选项"
},
"similar_notes": {
"title": "相似笔记",
@@ -1064,11 +1068,6 @@
"note_detail_render_help_1": "之所以显示此帮助说明,是因为这个类型为渲染 HTML 的笔记没有正常工作所需的关系。",
"note_detail_render_help_2": "渲染 HTML 笔记类型用于<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">编写脚本</a>。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript然后这个笔记会把页面渲染出来。要使其正常工作您需要定义一个名为 \"renderNote\" 的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">关系</a>指向要渲染的 HTML 笔记。"
},
"web_view": {
"web_view": "网页视图",
"embed_websites": "网页视图类型的笔记允许您将网站嵌入到 Trilium 中。",
"create_label": "首先,请创建一个带有您要嵌入的 URL 地址的标签,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "刷新"
},
@@ -1417,7 +1416,8 @@
"description": "描述",
"reload_app": "重载应用以应用更改",
"set_all_to_default": "将所有快捷键重置为默认值",
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?"
"confirm_reset": "您确定要将所有键盘快捷键重置为默认值吗?",
"no_results": "未找到与“{{filter}}”匹配的快捷方式"
},
"spellcheck": {
"title": "拼写检查",
@@ -1618,7 +1618,9 @@
"print_report_title": "打印报告",
"print_report_collection_content_other": "集合中的 {{count}} 篇笔记无法打印,因为它们不受支持或受到保护。",
"print_report_collection_details_button": "查看详情",
"print_report_collection_details_ignored_notes": "忽略的笔记"
"print_report_collection_details_ignored_notes": "忽略的笔记",
"print_report_error_title": "打印失败",
"print_report_stack_trace": "堆栈跟踪"
},
"note_title": {
"placeholder": "请输入笔记标题...",
@@ -1782,8 +1784,8 @@
"desktop-application": "桌面应用程序",
"native-title-bar": "原生标题栏",
"native-title-bar-description": "对于 Windows 和 macOS关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。",
"background-effects": "启用背景效果(仅适用于 Windows 11",
"background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
"background-effects": "启用背景效果",
"background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
"restart-app-button": "重启应用程序以查看更改",
"zoom-factor": "缩放系数"
},
@@ -1802,7 +1804,8 @@
"geo-map": {
"create-child-note-title": "创建一个新的子笔记并将其添加到地图中",
"create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。",
"unable-to-load-map": "无法加载地图。"
"unable-to-load-map": "无法加载地图。",
"create-child-note-text": "添加标记"
},
"geo-map-context": {
"open-location": "打开位置",
@@ -2117,7 +2120,7 @@
},
"call_to_action": {
"background_effects_title": "背景效果现已推出稳定版本",
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
"background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。",
"background_effects_button": "启用背景效果",
"next_theme_title": "试用新 Trilium 主题",
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
@@ -2253,5 +2256,22 @@
"pages_alt": "第{{pageNumber}}页",
"pages_loading": "加载中...",
"layers_other": "{{count}} 层"
},
"platform_indicator": {
"available_on": "在 {{platform}} 上可用"
},
"mobile_tab_switcher": {
"title_other": "{{count}} 选项卡",
"more_options": "更多选项"
},
"bookmark_buttons": {
"bookmarks": "书签"
},
"web_view_setup": {
"title": "直接在 Trilium 中创建网页的实时视图",
"url_placeholder": "输入或粘贴网站地址,例如 https://triliumnotes.org",
"create_button": "创建网页视图",
"invalid_url_title": "无效的地址",
"invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。"
}
}

View File

@@ -1,6 +1,6 @@
{
"about": {
"title": "Über Trilium Notizen",
"title": "Über Trilium Notes",
"homepage": "Startseite:",
"app_version": "App-Version:",
"db_version": "DB-Version:",
@@ -662,7 +662,8 @@
"show-cheatsheet": "Cheatsheet anzeigen",
"toggle-zen-mode": "Zen Modus",
"new-version-available": "Neues Update verfügbar",
"download-update": "Version {{latestVersion}} herunterladen"
"download-update": "Version {{latestVersion}} herunterladen",
"search_notes": "Notizen durchsuchen"
},
"sync_status": {
"unknown": "<p>Der Synchronisations-Status wird bekannt, sobald der nächste Synchronisierungsversuch gestartet wird.</p><p>Klicke, um eine Synchronisierung jetzt auszulösen.</p>",
@@ -742,7 +743,7 @@
"button_title": "Diagramm als SVG exportieren"
},
"relation_map_buttons": {
"create_child_note_title": "Erstelle eine neue untergeordnete Notiz und füge sie dieser Beziehungskarte hinzu",
"create_child_note_title": "Erstelle eine untergeordnete Notiz und füge sie dieser Karte hinzu",
"reset_pan_zoom_title": "Schwenken und Zoomen auf die ursprünglichen Koordinaten und Vergrößerung zurücksetzen",
"zoom_in_title": "Hineinzoom",
"zoom_out_title": "Herauszoomen"
@@ -757,7 +758,9 @@
"delete_this_note": "Diese Notiz löschen",
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
"error_unrecognized_command": "Unbekannter Befehl {{command}}",
"note_revisions": "Notiz Revisionen"
"note_revisions": "Notiz Revisionen",
"backlinks": "Rücklinks",
"content_language_switcher": "Inhaltssprache: {{language}}"
},
"note_icon": {
"change_note_icon": "Notiz-Icon ändern",
@@ -909,7 +912,8 @@
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
"actions_executed": "Aktionen wurden ausgeführt.",
"view_options": "Optionen anzeigen:"
"view_options": "Optionen anzeigen:",
"option": "Option"
},
"similar_notes": {
"title": "Ähnliche Notizen",
@@ -1063,11 +1067,6 @@
"note_detail_render_help_1": "Diese Hilfesnotiz wird angezeigt, da diese Notiz vom Typ „HTML rendern“ nicht über die erforderliche Beziehung verfügt, um ordnungsgemäß zu funktionieren.",
"note_detail_render_help_2": "Render-HTML-Notiztyp wird benutzt für <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">Beziehung</a> namens \"renderNote\" zeigend auf die HTML-Notiz zum rendern definieren."
},
"web_view": {
"web_view": "Webansicht",
"embed_websites": "Notiz vom Typ Web View ermöglicht das Einbetten von Websites in Trilium.",
"create_label": "Um zu beginnen, erstelle bitte ein Label mit einer URL-Adresse, die eingebettet werden soll, z. B. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Aktualisieren"
},
@@ -1383,7 +1382,8 @@
"description": "Beschreibung",
"reload_app": "Lade die App neu, um die Änderungen zu übernehmen",
"set_all_to_default": "Setze alle Verknüpfungen auf die Standardeinstellungen",
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?"
"confirm_reset": "Möchtest du wirklich alle Tastaturkürzel auf die Standardeinstellungen zurücksetzen?",
"no_results": "Keine Tastenkürzel für '{{filter}}' gefunden"
},
"spellcheck": {
"title": "Rechtschreibprüfung",
@@ -1587,7 +1587,9 @@
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind."
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind.",
"print_report_error_title": "Druck fehlgeschlagen",
"print_report_stack_trace": "Stapelzurückverfolgung"
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…",
@@ -1771,7 +1773,8 @@
"geo-map": {
"create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen",
"create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.",
"unable-to-load-map": "Karte konnte nicht geladen werden."
"unable-to-load-map": "Karte konnte nicht geladen werden.",
"create-child-note-text": "Marker hinzufügen"
},
"geo-map-context": {
"open-location": "Ort öffnen",
@@ -2270,5 +2273,20 @@
},
"platform_indicator": {
"available_on": "Verfügbar auf {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} Tab",
"title_other": "{{count}} Tabs",
"more_options": "Weitere Optionen"
},
"bookmark_buttons": {
"bookmarks": "Lesezeichen"
},
"web_view_setup": {
"title": "Erstelle eine Live-Ansicht einer Webseite direkt in Trilium",
"url_placeholder": "Gib oder füge die Adresse der Webseite ein, zum Beispiel https://triliumnotes.org",
"create_button": "Erstelle Web Ansicht",
"invalid_url_title": "Ungültige Adresse",
"invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org."
}
}

View File

@@ -13,6 +13,61 @@
"critical-error": {
"title": "Κρίσιμο σφάλμα",
"message": "Συνέβη κάποιο κρίσιμο σφάλμα, το οποίο δεν επιτρέπει στην εφαρμογή χρήστη να ξεκινήσει:\n\n{{message}}\n\nΤο πιθανότερο είναι να προκλήθηκε από κάποιο script που απέτυχε απρόοπτα. Δοκιμάστε να ξεκινήσετε την εφαρμογή σε ασφαλή λειτουργία για να λύσετε το πρόβλημα."
}
},
"widget-error": {
"title": "Δεν ήταν δυνατή η αρχικοποίηση του widget",
"message-custom": "Προσαρμοσμένο widget της σημείωσης με ID \"{{id}}\", με τίτλο \"{{title}}\", δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}",
"message-unknown": "Άγνωστο widget δεν ήταν δυνατό να αρχικοποιηθεί λόγω:\n\n{{message}}"
},
"bundle-error": {
"title": "Δεν ήταν δυνατή η φόρτωση προσαρμοσμένου script",
"message": "Το script δεν ήταν δυνατό να εκτελεστεί λόγω:\n\n{{message}}"
},
"widget-list-error": {
"title": "Δεν ήταν δυνατή η λήψη της λίστας των widgets από τον server"
},
"widget-render-error": {
"title": "Δεν ήταν δυνατή η απόδοση προσαρμοσμένου React widget"
},
"widget-missing-parent": "Το προσαρμοσμένο widget δεν έχει ορισμένη την υποχρεωτική ιδιότητα '{{property}}'.\n\nΕάν το script προορίζεται για εκτέλεση χωρίς UI element, χρησιμοποιήστε '#run=frontendStartup' αντί για αυτό.",
"open-script-note": "Άνοιγμα σημείωσης script",
"scripting-error": "Σφάλμα προσαρμοσμένου script: {{title}}"
},
"bookmark_buttons": {
"bookmarks": "Σελιδοδείκτες"
},
"add_link": {
"add_link": "Προσθήκη συνδέσμου",
"help_on_links": "Βοήθεια για συνδέσμους",
"note": "Σημείωση",
"search_note": "Αναζήτηση σημείωσης με βάση το όνομά της",
"link_title_mirrors": "Ο τίτλος του συνδέσμου αντικατοπτρίζει τον τρέχοντα τίτλο της σημείωσης",
"link_title_arbitrary": "Ο τίτλος του συνδέσμου μπορεί να τροποποιηθεί ελεύθερα",
"link_title": "Τίτλος συνδέσμου",
"button_add_link": "Προσθήκη συνδέσμου"
},
"branch_prefix": {
"edit_branch_prefix": "Επεξεργασία προθέματος κλάδου",
"edit_branch_prefix_multiple": "Επεξεργασία προθέματος κλάδου για {{count}} κλάδους",
"help_on_tree_prefix": "Βοήθεια για πρόθεμα δέντρου",
"prefix": "Πρόθεμα: ",
"save": "Αποθήκευση",
"branch_prefix_saved": "Το πρόθεμα κλάδου αποθηκεύτηκε.",
"branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους.",
"affected_branches": "Επηρεαζόμενοι κλάδοι ({{count}}):"
},
"bulk_actions": {
"bulk_actions": "Μαζικές ενέργειες",
"affected_notes": "Επηρεαζόμενες σημειώσεις",
"include_descendants": "Συμπερίληψη απογόνων των επιλεγμένων σημειώσεων",
"available_actions": "Διαθέσιμες ενέργειες",
"chosen_actions": "Επιλεγμένες ενέργειες",
"execute_bulk_actions": "Εκτέλεση μαζικών ενεργειών",
"bulk_actions_executed": "Οι μαζικές ενέργειες εκτελέστηκαν επιτυχώς.",
"none_yet": "Καμία ακόμη… προσθέστε μια ενέργεια επιλέγοντας μία από τις διαθέσιμες παραπάνω.",
"labels": "Ετικέτες",
"relations": "Συσχετίσεις",
"notes": "Σημειώσεις",
"other": "Λοιπά"
}
}

View File

@@ -662,7 +662,8 @@
"show-cheatsheet": "Show Cheatsheet",
"toggle-zen-mode": "Zen Mode",
"new-version-available": "New Update Available",
"download-update": "Get Version {{latestVersion}}"
"download-update": "Get Version {{latestVersion}}",
"search_notes": "Search notes"
},
"zen_mode": {
"button_exit": "Exit Zen Mode"
@@ -745,7 +746,7 @@
"button_title": "Export diagram as SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Create new child note and add it into this relation map",
"create_child_note_title": "Create child note and add it to map",
"reset_pan_zoom_title": "Reset pan & zoom to initial coordinates and magnification",
"zoom_in_title": "Zoom In",
"zoom_out_title": "Zoom Out"
@@ -760,7 +761,9 @@
"delete_this_note": "Delete this note",
"note_revisions": "Note revisions",
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
"error_unrecognized_command": "Unrecognized command {{command}}"
"error_unrecognized_command": "Unrecognized command {{command}}",
"backlinks": "Backlinks",
"content_language_switcher": "Content language: {{language}}"
},
"note_icon": {
"change_note_icon": "Change note icon",
@@ -905,6 +908,7 @@
"debug": "debug",
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
"action": "action",
"option": "option",
"search_button": "Search",
"search_execute": "Search & Execute actions",
"save_to_note": "Save to note",
@@ -1006,7 +1010,7 @@
"no_attachments": "This note has no attachments."
},
"book": {
"no_children_help": "This collection doesn't have any child notes so there's nothing to display. See <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> for details.",
"no_children_help": "This collection doesn't have any child notes so there's nothing to display.",
"drag_locked_title": "Locked for editing",
"drag_locked_message": "Dragging not allowed since the collection is locked for editing."
},
@@ -1063,13 +1067,21 @@
"click_on_canvas_to_place_new_note": "Click on canvas to place new note"
},
"render": {
"note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.",
"note_detail_render_help_2": "Render HTML note type is used for <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> called \"renderNote\" pointing to the HTML note to render."
"setup_title": "Display custom HTML or Preact JSX inside this note",
"setup_create_sample_preact": "Create sample note with Preact",
"setup_create_sample_html": "Create sample note with HTML",
"setup_sample_created": "A sample note was created as a child note.",
"disabled_description": "This render notes comes from an external source. To protect you from malicious content, it is not enabled by default. Make sure you trust the source before enabling it.",
"disabled_button_enable": "Enable render note"
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Note of type Web View allows you to embed websites into Trilium.",
"create_label": "To start, please create a label with a URL address you want to embed, e.g. #webViewSrc=\"https://www.google.com\""
"web_view_setup": {
"title": "Create a live view of a webpage directly into Trilium",
"url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org",
"create_button": "Create Web View",
"invalid_url_title": "Invalid address",
"invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.",
"disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isnt loading automatically. You can enable it if you trust the source.",
"disabled_button_enable": "Enable web view"
},
"backend_log": {
"refresh": "Refresh"
@@ -1585,7 +1597,8 @@
"description": "Description",
"reload_app": "Reload app to apply changes",
"set_all_to_default": "Set all shortcuts to the default",
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?"
"confirm_reset": "Do you really want to reset all keyboard shortcuts to the default?",
"no_results": "No shortcuts found matching '{{filter}}'"
},
"spellcheck": {
"title": "Spell Check",
@@ -1791,6 +1804,8 @@
"printing": "Printing in progress...",
"printing_pdf": "Exporting to PDF in progress...",
"print_report_title": "Print report",
"print_report_error_title": "Failed to print",
"print_report_stack_trace": "Stack trace",
"print_report_collection_content_one": "{{count}} note in the collection could not be printed because they are not supported or they are protected.",
"print_report_collection_content_other": "{{count}} notes in the collection could not be printed because they are not supported or they are protected.",
"print_report_collection_details_button": "See details",
@@ -2095,7 +2110,8 @@
"raster": "Raster",
"vector_light": "Vector (Light)",
"vector_dark": "Vector (Dark)",
"show-scale": "Show scale"
"show-scale": "Show scale",
"show-labels": "Show marker names"
},
"table_context_menu": {
"delete_row": "Delete row"
@@ -2276,5 +2292,35 @@
"title_one": "{{count}} tab",
"title_other": "{{count}} tabs",
"more_options": "More options"
},
"bookmark_buttons": {
"bookmarks": "Bookmarks"
},
"active_content_badges": {
"type_icon_pack": "Icon pack",
"type_backend_script": "Backend script",
"type_frontend_script": "Frontend script",
"type_widget": "Widget",
"type_app_css": "Custom CSS",
"type_render_note": "Render note",
"type_web_view": "Web view",
"type_app_theme": "Custom theme",
"toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.",
"toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.",
"menu_docs": "Open documentation",
"menu_execute_now": "Execute script now",
"menu_run": "Run automatically",
"menu_run_disabled": "Manually",
"menu_run_backend_startup": "When the backend starts up",
"menu_run_hourly": "Hourly",
"menu_run_daily": "Daily",
"menu_run_frontend_startup": "When the desktop frontend starts up",
"menu_run_mobile_startup": "When the mobile frontend starts up",
"menu_change_to_widget": "Change to widget",
"menu_change_to_frontend_script": "Change to frontend script",
"menu_theme_base": "Theme base"
},
"setup_form": {
"more_info": "Learn more"
}
}

View File

@@ -662,7 +662,8 @@
"show-cheatsheet": "Mostrar hoja de trucos",
"toggle-zen-mode": "Modo Zen",
"new-version-available": "Nueva actualización disponible",
"download-update": "Obtener versión {{latestVersion}}"
"download-update": "Obtener versión {{latestVersion}}",
"search_notes": "Buscar notas"
},
"zen_mode": {
"button_exit": "Salir del modo Zen"
@@ -745,7 +746,7 @@
"button_title": "Exportar diagrama como SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Crear una nueva subnota y agregarla a este mapa de relaciones",
"create_child_note_title": "Crear una subnota y agregarla al mapa",
"reset_pan_zoom_title": "Restablecer la panorámica y el zoom a las coordenadas y ampliación iniciales",
"zoom_in_title": "Acercar",
"zoom_out_title": "Alejar"
@@ -754,14 +755,16 @@
"relation": "relación",
"backlink_one": "{{count}} Vínculo de retroceso",
"backlink_many": "{{count}} Vínculos de retroceso",
"backlink_other": "{{count}} vínculos de retroceso"
"backlink_other": "{{count}} Vínculos de retroceso"
},
"mobile_detail_menu": {
"insert_child_note": "Insertar subnota",
"delete_this_note": "Eliminar esta nota",
"error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'",
"error_unrecognized_command": "Comando no reconocido {{command}}",
"note_revisions": "Revisiones de notas"
"note_revisions": "Revisiones de notas",
"backlinks": "Vínculos de retroceso",
"content_language_switcher": "Idioma de contenido: {{language}}"
},
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
@@ -914,7 +917,8 @@
"unknown_search_option": "Opción de búsqueda desconocida {{searchOptionName}}",
"search_note_saved": "La nota de búsqueda se ha guardado en {{- notePathTitle}}",
"actions_executed": "Las acciones han sido ejecutadas.",
"view_options": "Ver opciones:"
"view_options": "Ver opciones:",
"option": "opción"
},
"similar_notes": {
"title": "Notas similares",
@@ -1068,11 +1072,6 @@
"note_detail_render_help_1": "Esta nota de ayuda se muestra porque esta nota de tipo Renderizar HTML no tiene la relación requerida para funcionar correctamente.",
"note_detail_render_help_2": "El tipo de nota Render HTML es usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. De forma resumida, tiene una nota con código HTML (opcionalmente con algo de JavaScript) y esta nota la renderizará. Para que funcione, es necesario definir una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relación</a> llamada \"renderNote\" apuntando a la nota HTML nota a renderizar."
},
"web_view": {
"web_view": "Vista web",
"embed_websites": "La nota de tipo Web View le permite insertar sitios web en Trilium.",
"create_label": "Para comenzar, por favor cree una etiqueta con una dirección URL que desee empotrar, e.g. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Refrescar"
},
@@ -1573,7 +1572,8 @@
"description": "Descripción",
"reload_app": "Vuelva a cargar la aplicación para aplicar los cambios",
"set_all_to_default": "Establecer todos los accesos directos al valor predeterminado",
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?"
"confirm_reset": "¿Realmente desea restablecer todos los atajos de teclado a sus valores predeterminados?",
"no_results": "No se encontraron atajos que coincidan con '{{filter}} '"
},
"spellcheck": {
"title": "Revisión ortográfica",
@@ -1780,7 +1780,9 @@
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_title": "Imprimir informe",
"print_report_collection_details_button": "Ver detalles",
"print_report_collection_details_ignored_notes": "Notas ignoradas"
"print_report_collection_details_ignored_notes": "Notas ignoradas",
"print_report_stack_trace": "Rastreo de pila",
"print_report_error_title": "Fallo al imprimir"
},
"note_title": {
"placeholder": "escriba el título de la nota aquí...",
@@ -2285,5 +2287,21 @@
},
"platform_indicator": {
"available_on": "Disponible en {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} pestaña",
"title_many": "{{count}} pestañas",
"title_other": "{{count}} pestañas",
"more_options": "Más opciones"
},
"bookmark_buttons": {
"bookmarks": "Marcadores"
},
"web_view_setup": {
"title": "Crear una vista en vivo de una página web directamente en Trilium",
"url_placeholder": "Ingresar o pegar la dirección del sitio web, por ejemplo https://triliumnotes.org",
"create_button": "Crear Vista Web",
"invalid_url_title": "Dirección inválida",
"invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org."
}
}

View File

@@ -1057,11 +1057,6 @@
"note_detail_render_help_1": "Cette note d'aide s'affiche car cette note de type Rendu HTML n'a pas la relation requise pour fonctionner correctement.",
"note_detail_render_help_2": "Le type de note Rendu HTML est utilisé pour les <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripts</a>. En résumé, vous disposez d'une note de code HTML (éventuellement contenant JavaScript) et cette note affichera le rendu. Pour que cela fonctionne, vous devez définir une <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> appelée \"renderNote\" pointant vers la note HTML à rendre."
},
"web_view": {
"web_view": "Affichage Web",
"embed_websites": "Les notes de type Affichage Web vous permet d'intégrer des sites Web dans Trilium.",
"create_label": "Pour commencer, veuillez créer un label avec l'adresse URL que vous souhaitez intégrer, par ex. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Rafraîchir"
},

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,8 @@
"save": "Simpan",
"branch_prefix_saved": "Prefiks cabang telah disimpan.",
"branch_prefix_saved_multiple": "Prefix cabang telah disimpan pada {{count}} cabang.",
"affected_branches": "Cabang terdampak ({{count}}):"
"affected_branches": "Cabang terdampak ({{count}}):",
"edit_branch_prefix": "Sunting awalan cabang"
},
"bulk_actions": {
"bulk_actions": "Aksi borongan",
@@ -61,7 +62,10 @@
"execute_bulk_actions": "Eksekusi aksi borongan",
"bulk_actions_executed": "Aksi borongan telah di eksekusi dengan sukses.",
"none_yet": "Belum ada... tambahkan aksi dengan memilih salah satu dari aksi di atas.",
"labels": "Label-label"
"labels": "Label-label",
"relations": "Hubungan",
"notes": "Catatan",
"other": "Lainnya"
},
"confirm": {
"cancel": "Batal",
@@ -80,6 +84,8 @@
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat)."
},
"clone_to": {
"clone_notes_to": "Duplikat catatan ke…"
"clone_notes_to": "Duplikat catatan ke…",
"help_on_links": "Bantuan pada tautan",
"notes_to_clone": "Catatan untuk kloning"
}
}

View File

@@ -186,7 +186,8 @@
"geo-map": {
"create-child-note-title": "Crea una nota figlia e aggiungila alla mappa",
"create-child-note-instruction": "Clicca sulla mappa per creare una nuova nota qui o premi Escape per uscire.",
"unable-to-load-map": "Impossibile caricare la mappa."
"unable-to-load-map": "Impossibile caricare la mappa.",
"create-child-note-text": "Aggiungi indicatore"
},
"geo-map-context": {
"open-location": "Apri la posizione",
@@ -422,7 +423,8 @@
"unknown_search_option": "Opzione di ricerca sconosciuta {{searchOptionName}}",
"search_note_saved": "La nota di ricerca è stata salvata in {{- notePathTitle}}",
"actions_executed": "Le azioni sono state eseguite.",
"view_options": "Opzioni di visualizzazione:"
"view_options": "Opzioni di visualizzazione:",
"option": "opzione"
},
"modal": {
"close": "Chiudi",
@@ -1241,7 +1243,8 @@
"show-cheatsheet": "Mostra il foglietto illustrativo",
"toggle-zen-mode": "Modalità Zen",
"new-version-available": "Nuovo aggiornamento disponibile",
"download-update": "Ottieni la versione {{latestVersion}}"
"download-update": "Ottieni la versione {{latestVersion}}",
"search_notes": "Cerca note"
},
"zen_mode": {
"button_exit": "Esci dalla modalità Zen"
@@ -1340,7 +1343,9 @@
"delete_this_note": "Elimina questa nota",
"note_revisions": "Revisioni delle note",
"error_cannot_get_branch_id": "Impossibile ottenere branchId per notePath '{{notePath}}'",
"error_unrecognized_command": "Comando non riconosciuto {{command}}"
"error_unrecognized_command": "Comando non riconosciuto {{command}}",
"backlinks": "Backlinks",
"content_language_switcher": "Lingua dei contenuti: {{language}}"
},
"note_icon": {
"change_note_icon": "Cambia icona nota",
@@ -1581,11 +1586,6 @@
"note_detail_render_help_1": "Questa nota di aiuto viene visualizzata perché questa nota di tipo Render HTML non ha la relazione richiesta per funzionare correttamente.",
"note_detail_render_help_2": "Il tipo di nota HTML Render viene utilizzato per lo <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relazione</a> denominata \"renderNote\" che punti alla nota HTML da visualizzare."
},
"web_view": {
"web_view": "Visualizzazione Web",
"embed_websites": "La nota di tipo Web View consente di incorporare siti web in Trilium.",
"create_label": "Per iniziare, crea un'etichetta con l'indirizzo URL che desideri incorporare, ad esempio #webViewSrc=\"https://www.google.com\""
},
"vacuum_database": {
"title": "Pulizia del database",
"description": "Questa operazione ricostruirà il database, generando in genere un file di dimensioni inferiori. In realtà, nessun dato verrà modificato.",
@@ -2145,7 +2145,7 @@
"background_effects_title": "Gli effetti di sfondo sono ora stabili",
"background_effects_message": "Sui dispositivi Windows, gli effetti di sfondo sono ora completamente stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo retrostante. Questa tecnica è utilizzata anche in altre applicazioni come Esplora risorse di Windows.",
"background_effects_button": "Abilita gli effetti di sfondo",
"dismiss": "Congedare",
"dismiss": "Chiudi",
"new_layout_title": "Nuovo layout",
"new_layout_message": "Abbiamo introdotto un layout modernizzato per Trilium. La barra multifunzione è stata rimossa e integrata perfettamente nell'interfaccia principale, con una nuova barra di stato e sezioni espandibili (come gli attributi promossi) che assumono le funzioni chiave.\n\nIl nuovo layout è abilitato di default e può essere temporaneamente disabilitato tramite Opzioni → Aspetto.",
"new_layout_button": "Maggiori informazioni"
@@ -2281,5 +2281,24 @@
"pages_other": "{{count}} pagine",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Caricamento in corso..."
},
"web_view_setup": {
"title": "Crea una visualizzazione live di una pagina web direttamente in Trilium",
"url_placeholder": "Inserisci o incolla l'indirizzo del sito web, ad esempio https://triliumnotes.org",
"create_button": "Crea vista Web",
"invalid_url_title": "Indirizzo non valido",
"invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org."
},
"platform_indicator": {
"available_on": "Disponibile su {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "Scheda {{count}}",
"title_many": "Schede {{count}}",
"title_other": "Schede {{count}}",
"more_options": "Altre opzioni"
},
"bookmark_buttons": {
"bookmarks": "Segnalibri"
}
}

View File

@@ -81,7 +81,8 @@
"configure_launchbar": "ランチャーバーの設定",
"show_shared_notes_subtree": "共有ノートのサブツリーを表示",
"new-version-available": "新しいアップデートが利用可能",
"download-update": "{{latestVersion}} をバージョンを入手"
"download-update": "{{latestVersion}} をバージョンを入手",
"search_notes": "検索ノート"
},
"left_pane_toggle": {
"show_panel": "パネルを表示",
@@ -234,7 +235,8 @@
"search_note_saved": "検索ノートが {{- notePathTitle}} に保存されました",
"actions_executed": "アクションが実行されました。",
"ancestor": "祖先:",
"view_options": "表示オプション:"
"view_options": "表示オプション:",
"option": "オプション"
},
"shortcuts": {
"multiple_shortcuts": "同じアクションに対して複数のショートカットを設定する場合、カンマで区切ることができます。",
@@ -247,7 +249,8 @@
"reload_app": "リロードして変更を適用する",
"set_all_to_default": "すべてのショートカットをデフォルトに戻す",
"confirm_reset": "キーボードショートカットをすべてデフォルトにリセットしますか?",
"keyboard_shortcuts": "キーボードショートカット"
"keyboard_shortcuts": "キーボードショートカット",
"no_results": "'{{filter}}' に一致するショートカットが見つかりません"
},
"confirm": {
"confirmation": "確認",
@@ -407,7 +410,7 @@
"relation_map_buttons": {
"zoom_out_title": "ズームアウト",
"zoom_in_title": "ズームイン",
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
"create_child_note_title": "子ノートを作成し、マップに追加",
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
},
"tree-context-menu": {
@@ -824,11 +827,6 @@
"error_no_path": "移動するパスがありません。",
"move_success_message": "選択したノートは以下に移動されました "
},
"web_view": {
"web_view": "Web ビュー",
"embed_websites": "Web ビュータイプでは、web サイトを Trilium に埋め込むことができます。",
"create_label": "まず始めに、埋め込みたいURLアドレスのラベルを作成してください。例: #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "リフレッシュ"
},
@@ -1648,7 +1646,9 @@
"error_unrecognized_command": "認識されないコマンド {{command}}",
"insert_child_note": "子ノートを挿入",
"error_cannot_get_branch_id": "ノートパス 「{{notePath}} のbranchIdを取得できません",
"note_revisions": "ノートの変更履歴"
"note_revisions": "ノートの変更履歴",
"backlinks": "バックリンク",
"content_language_switcher": "コンテンツの言語: {{language}}"
},
"inherited_attribute_list": {
"title": "継承属性",
@@ -1955,7 +1955,9 @@
"print_report_title": "レポートを印刷",
"print_report_collection_content_other": "コレクション内の {{count}} 件のノートは、サポートされていないか保護されているため、印刷できませんでした。",
"print_report_collection_details_button": "詳細を見る",
"print_report_collection_details_ignored_notes": "無視されたノート"
"print_report_collection_details_ignored_notes": "無視されたノート",
"print_report_error_title": "印刷に失敗しました",
"print_report_stack_trace": "スタックトレース"
},
"watched_file_update_status": {
"ignore_this_change": "この変更を無視する",
@@ -2008,7 +2010,8 @@
"geo-map": {
"create-child-note-title": "新しい子ノートを作成し、マップに追加する",
"create-child-note-instruction": "地図をクリックしてその場所に新しいートを作成するか、Esc キーを押して閉じます。",
"unable-to-load-map": "マップを読み込めません。"
"unable-to-load-map": "マップを読み込めません。",
"create-child-note-text": "マーカーを追加"
},
"geo-map-context": {
"open-location": "現在位置を表示",
@@ -2256,5 +2259,19 @@
},
"platform_indicator": {
"available_on": "{{platform}} で利用可能"
},
"mobile_tab_switcher": {
"title_other": "{{count}} タブ",
"more_options": "その他のオプション"
},
"bookmark_buttons": {
"bookmarks": "ブックマーク"
},
"web_view_setup": {
"title": "Trilium に直接 Web ページのライブビューを作成",
"url_placeholder": "Web サイトのアドレスを入力または貼り付けて下さい。 例: https://triliumnotes.org",
"create_button": "Web ビューを作成",
"invalid_url_title": "無効なアドレス",
"invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org"
}
}

View File

@@ -21,7 +21,7 @@
},
"bundle-error": {
"title": "Nie udało się załadować niestandardowego skryptu",
"message": "Skrypt z notatki o ID \"{{id}}\", zatytułowany \"{{title}}\", nie mógł zostać wykonany z powodu:\n\n{{message}}"
"message": "Skrypt nie mógł zostać wykonany z powodu:\n\n{{message}}"
},
"widget-list-error": {
"title": "Nie udało się pobrać listy widżetów z serwera"
@@ -29,8 +29,9 @@
"widget-render-error": {
"title": "Nie udało się wyrenderować niestandardowego widżetu React"
},
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.",
"open-script-note": "Otwórz notatkę ze skryptem"
"widget-missing-parent": "Niestandardowy widżet nie ma zdefiniowanej obowiązkowej właściwości „{{property}}”.\nJeśli skrypt ma działać bez interfejsu użytkownika (UI) wyłącz go: '#run=frontendStartup'.",
"open-script-note": "Otwórz notatkę ze skryptem",
"scripting-error": "Błąd skryptu użytkownika: {{title}}"
},
"add_link": {
"add_link": "Dodaj link",
@@ -191,7 +192,8 @@
"expand_tooltip": "Rozwija bezpośrednie elementy podrzędne tej kolekcji (o jeden poziom). Aby uzyskać więcej opcji, naciśnij strzałkę po prawej.",
"expand_first_level": "Rozwiń bezpośrednie elementy podrzędne",
"expand_nth_level": "Rozwiń {{depth}} poziomów",
"expand_all_levels": "Rozwiń wszystkie poziomy"
"expand_all_levels": "Rozwiń wszystkie poziomy",
"hide_child_notes": "Ukryj notatki podrzędne w derzwie"
},
"board_view": {
"move-to": "Przenieś do",
@@ -240,7 +242,7 @@
"background_effects_title": "Efekty tła są teraz stabilne",
"dismiss": "Odrzuć",
"background_effects_button": "Włącz efekty tła",
"background_effects_message": "Na urządzeniach z systemem Windows efekty tła są teraz w pełni stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim. Ta technika jest również stosowana w innych aplikacjach, takich jak Eksplorator Windows.",
"background_effects_message": "Na urządzeniach z systemem Windows i macOS efekty tła są stabilne. Efekty tła dodają odrobinę koloru do interfejsu użytkownika poprzez rozmycie tła za nim.",
"new_layout_title": "Nowy układ",
"new_layout_message": "Wprowadziliśmy zmodernizowany układ interfejsu dla Trilium. Wstążka została usunięta i płynnie zintegrowana z głównym interfejsem, a jej kluczowe funkcje przejęły nowy pasek stanu i rozwijane sekcje (takie jak promowane atrybuty).\n\nNowy układ jest domyślnie włączony i można go tymczasowo wyłączyć w Ustawienia → Wygląd.",
"new_layout_button": "Szczegóły"
@@ -520,7 +522,8 @@
"action": "akcja",
"search_button": "Szukaj",
"search_execute": "Szukaj i wykonaj akcje",
"view_options": "Ustawienia widoku:"
"view_options": "Ustawienia widoku:",
"option": "opcja"
},
"similar_notes": {
"title": "Podobne notatki",
@@ -602,8 +605,8 @@
"desktop-application": "Aplikacja desktopowa",
"native-title-bar": "Natywny pasek tytułu",
"native-title-bar-description": "Dla Windows i macOS, wyłączenie natywnego paska tytułu sprawia, że aplikacja wygląda bardziej kompaktowo. Na Linuxie, włączenie natywnego paska tytułu lepiej integruje się z resztą systemu.",
"background-effects": "Włącz efekty tła (tylko Windows 11)",
"background-effects-description": "Efekt Mica dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
"background-effects": "Włącz efekty tła",
"background-effects-description": "Dodaje rozmyte, stylowe tło do okien aplikacji, tworząc głębię i nowoczesny wygląd. \"Natywny pasek tytułu\" musi być wyłączony.",
"restart-app-button": "Zrestartuj aplikację, aby zobaczyć zmiany",
"zoom-factor": "Współczynnik powiększenia"
},
@@ -1182,7 +1185,8 @@
"show-cheatsheet": "Pokaż ściągawkę",
"toggle-zen-mode": "Tryb Zen",
"new-version-available": "Dostępna nowa aktualizacja",
"download-update": "Pobierz wersję {{latestVersion}}"
"download-update": "Pobierz wersję {{latestVersion}}",
"search_notes": "Przeszukaj notatki"
},
"zen_mode": {
"button_exit": "Wyjdź z trybu Zen"
@@ -1265,7 +1269,7 @@
"button_title": "Eksportuj diagram jako SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Utwórz nową notatkę podrzędną i dodaj ją do tej mapy relacji",
"create_child_note_title": "Utwórz notatkę podrzędną i dodaj ją do mapy",
"reset_pan_zoom_title": "Zresetuj przesunięcie i powiększenie do początkowych współrzędnych i powiększenia",
"zoom_in_title": "Powiększ",
"zoom_out_title": "Pomniejsz"
@@ -1281,12 +1285,23 @@
"delete_this_note": "Usuń tę notatkę",
"note_revisions": "Wersje notatki",
"error_cannot_get_branch_id": "Nie można pobrać branchId dla ścieżki notatki '{{notePath}}'",
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}"
"error_unrecognized_command": "Nierozpoznane polecenie {{command}}",
"backlinks": "Linki zwrotne",
"content_language_switcher": "Język treści: {{language}}"
},
"note_icon": {
"change_note_icon": "Zmień ikonę notatki",
"search": "Szukaj:",
"reset-default": "Przywróć domyślną ikonę"
"reset-default": "Przywróć domyślną ikonę",
"search_placeholder_one": "Znaleziono {{number}} ikonę w {{count}} pakietach",
"search_placeholder_few": "Znaleziono {{number}} ikon w {{count}} pakietach",
"search_placeholder_many": "Znaleziono {{number}} ikon w {{count}} pakietach",
"search_placeholder_filtered": "Wyszukaj {{number}} ikon w {{name}}",
"filter": "Filtr",
"filter-none": "Wszystkie ikony",
"filter-default": "Domyślne ikony",
"icon_tooltip": "{{name}}\npakiet ikon: {{iconPack}}",
"no_results": "Nie znaleziono ikon."
},
"basic_properties": {
"note_type": "Typ notatki",
@@ -1421,11 +1436,6 @@
"note_detail_render_help_1": "Ta notatka pomocy jest wyświetlana, ponieważ ta notatka typu Render HTML nie ma wymaganej relacji do poprawnego działania.",
"note_detail_render_help_2": "Typ notatki Render HTML jest używany do <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">skryptowania</a>. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relację</a> o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania."
},
"web_view": {
"web_view": "Widok WWW",
"embed_websites": "Notatka typu Widok WWW pozwala na osadzanie stron internetowych w Trilium.",
"create_label": "Aby rozpocząć, utwórz etykietę z adresem URL, który chcesz osadzić, np. #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Odśwież"
},
@@ -1826,7 +1836,7 @@
"will_be_deleted_in": "Ten załącznik zostanie automatycznie usunięty za {{time}}",
"will_be_deleted_soon": "Ten załącznik zostanie wkrótce automatycznie usunięty",
"deletion_reason": ", ponieważ załącznik nie jest podlinkowany w treści notatki. Aby zapobiec usunięciu, dodaj link do załącznika z powrotem do treści lub przekonwertuj załącznik na notatkę.",
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}",
"role_and_size": "Rola: {{role}}, Rozmiar: {{size}}, MIME: {{- mimeType}}",
"link_copied": "Link do załącznika skopiowany do schowka.",
"unrecognized_role": "Nierozpoznana rola załącznika '{{role}}'."
},
@@ -1880,7 +1890,10 @@
"apply-bulk-actions": "Zastosuj akcje masowe",
"converted-to-attachments": "{{count}} notatek zostało przekonwertowanych na załączniki.",
"convert-to-attachment-confirm": "Czy na pewno chcesz przekonwertować wybrane notatki na załączniki ich notatek nadrzędnych? Ta operacja dotyczy tylko notatek Obrazów, inne notatki zostaną pominięte.",
"open-in-popup": "Szybka edycja"
"open-in-popup": "Szybka edycja",
"open-in-a-new-window": "Otwórz w nowym oknie",
"hide-subtree": "Ukryj gałąź",
"show-subtree": "Rozwiń gałąź"
},
"shared_info": {
"shared_publicly": "Ta notatka jest udostępniona publicznie pod adresem {{- link}}.",
@@ -1971,7 +1984,17 @@
"create-child-note": "Utwórz notatkę podrzędną",
"unhoist": "Cofnij zawężenie",
"toggle-sidebar": "Przełącz pasek boczny",
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone."
"dropping-not-allowed": "Upuszczanie notatek w tej lokalizacji jest niedozwolone.",
"clone-indicator-tooltip": "Ta notatka ma {{- count}} notatek nadrzędnych: {{- parents}}",
"clone-indicator-tooltip-single": "Ta notatka jest sklonowana (1 dodatkowa notatka nadrzędna: {{- parent}})",
"shared-indicator-tooltip": "Ta notatka jest udostępniona publicznie",
"shared-indicator-tooltip-with-url": "Ta notatka jest udostępniana publicznie jako: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} notatka podrzędna ukryta w drzewie",
"subtree-hidden-tooltip_few": "{{count}} notatek podrzędnych ukrytych w drzewie",
"subtree-hidden-tooltip_many": "{{count}} notatek podrzędnych ukrytych w drzewie",
"subtree-hidden-moved-title": "Dodano do {{title}}",
"subtree-hidden-moved-description-collection": "Ta kolekcja ukrywa swoje notatki podrzędne w drzewie.",
"subtree-hidden-moved-description-other": "Notatki podrzędne są ukryte w drzewie tej notatki."
},
"title_bar_buttons": {
"window-on-top": "Utrzymuj okno na wierzchu"
@@ -1979,7 +2002,13 @@
"note_detail": {
"could_not_find_typewidget": "Nie można znaleźć widżetu typu dla typu '{{type}}'",
"printing": "Drukowanie w toku...",
"printing_pdf": "Eksportowanie do PDF w toku..."
"printing_pdf": "Eksportowanie do PDF w toku...",
"print_report_title": "Wydrukuj raport",
"print_report_collection_content_one": "Nie można wydrukować {{count}} notatki w kolekcji, ponieważ nie jest ona obsługiwana lub jest chroniona.",
"print_report_collection_content_few": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
"print_report_collection_content_many": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
"print_report_collection_details_button": "Zobacz szczegóły",
"print_report_collection_details_ignored_notes": "Zignorowane notatki"
},
"note_title": {
"placeholder": "wpisz tytuł notatki tutaj...",
@@ -1989,7 +2018,8 @@
"note_type_switcher_others": "Inny typ notatki",
"note_type_switcher_templates": "Szablon",
"note_type_switcher_collection": "Kolekcja",
"edited_notes": "Edytowane notatki"
"edited_notes": "Notatki edytowane dzisiaj",
"promoted_attributes": "Sugerowane atrybuty"
},
"search_result": {
"no_notes_found": "Nie znaleziono notatek dla podanych parametrów wyszukiwania.",
@@ -1999,7 +2029,11 @@
"configure_launchbar": "Konfiguruj pasek szybkiego dostępu"
},
"sql_result": {
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy"
"no_rows": "Dla tego zapytania nie zwrócono żadnych wierszy",
"not_executed": "Zapytanie nie zostało jeszcze wykonane.",
"failed": "Wykonanie zapytania SQL nie powiodło się",
"statement_result": "Wynik wyrażenia",
"execute_now": "Wykonaj teraz"
},
"sql_table_schemas": {
"tables": "Tabele"
@@ -2116,7 +2150,8 @@
"geo-map": {
"create-child-note-title": "Utwórz nową notatkę podrzędną i dodaj ją do mapy",
"create-child-note-instruction": "Kliknij na mapie, aby utworzyć nową notatkę w tej lokalizacji lub naciśnij Escape, aby anulować.",
"unable-to-load-map": "Nie można załadować mapy."
"unable-to-load-map": "Nie można załadować mapy.",
"create-child-note-text": "Dodaj zaznaczenie"
},
"geo-map-context": {
"open-location": "Otwórz lokalizację",
@@ -2183,7 +2218,14 @@
"execute_sql_description": "Ta notatka jest notatką SQL. Kliknij, aby wykonać zapytanie SQL.",
"shared_copy_to_clipboard": "Kopiuj link do schowka",
"shared_open_in_browser": "Otwórz link w przeglądarce",
"shared_unshare": "Usuń udostępnienie"
"shared_unshare": "Usuń udostępnienie",
"save_status_saved": "Zapisane",
"save_status_saving": "Zapisywanie...",
"save_status_unsaved": "Niezapisane",
"save_status_error": "Zapis nie powiódł się",
"save_status_saving_tooltip": "Zmiany zostały zapisane.",
"save_status_unsaved_tooltip": "Są niezapisane zmiany. Zostaną one zapisane automatycznie za chwilę.",
"save_status_error_tooltip": "Wystąpił błąd podczas zapisywania notatki. Spróbuj skopiować treść notatki w inne miejsce i ponownie załadować aplikację."
},
"status_bar": {
"language_title": "Zmień język treści",
@@ -2226,5 +2268,30 @@
"empty_button": "Ukryj panel",
"toggle": "Pokaż/ukryj prawy panel",
"custom_widget_go_to_source": "Przejdź do kodu źródłowego"
},
"pdf": {
"attachments_one": "{{count}} załącznik",
"attachments_few": "{{count}} załączniki",
"attachments_many": "{{count}} załączników",
"layers_one": "{{count}} warstwa",
"layers_few": "{{count}} warstw",
"layers_many": "{{count}} warstw",
"pages_one": "{{count}} strona",
"pages_few": "{{count}} stron",
"pages_many": "{{count}} stron",
"pages_alt": "Strona {{pageNumber}}",
"pages_loading": "Wczytuję..."
},
"platform_indicator": {
"available_on": "Dostępne na {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} zakładka",
"title_few": "{{count}} zakładki",
"title_many": "{{count}} zakładek",
"more_options": "Więcej opcji"
},
"bookmark_buttons": {
"bookmarks": "Zakładki"
}
}

View File

@@ -1068,11 +1068,6 @@
"note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.",
"note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. Em suma, tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, deve definir uma <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> chamada \"renderNote\" que aponta para a nota HTML a ser renderizada."
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que incorpore sites no Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},

View File

@@ -1271,11 +1271,6 @@
"start_dragging_relations": "Comece arrastando as relações daqui e solte-as em outra nota.",
"cannot_match_transform": "Não foi possível combinar a transformação: {{transform}}"
},
"web_view": {
"web_view": "Web View",
"embed_websites": "Nota do tipo Visualização Web permite que você incorpore sites dentro do Trilium.",
"create_label": "Para começar, crie uma etiqueta com um endereço URL que deseja incorporar, por exemplo, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Recarregar"
},

View File

@@ -1376,11 +1376,6 @@
"enable_vim_keybindings": "Permite utilizarea combinațiilor de taste în stil Vim pentru notițele de tip cod (fără modul ex)",
"use_vim_keybindings_in_code_notes": "Combinații de taste Vim"
},
"web_view": {
"create_label": "Pentru a începe, creați o etichetă cu adresa URL de încorporat, e.g. #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Notițele de tip „Vizualizare web” permit încorporarea site-urilor web în Trilium.",
"web_view": "Vizualizare web"
},
"wrap_lines": {
"enable_line_wrap": "Activează trecerea automată pe rândul următor (poate necesita o reîncărcare a interfeței pentru a avea efect)",
"wrap_lines_in_code_notes": "Trecerea automată pe rândul următor în notițe de cod"
@@ -1761,8 +1756,8 @@
"show-recent-notes": "Afișează notițele recente"
},
"electron_integration": {
"background-effects": "Activează efectele de fundal (doar pentru Windows 11)",
"background-effects-description": "Efectul Mica adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
"background-effects": "Activează efectele de fundal",
"background-effects-description": "Adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
"desktop-application": "Aplicația desktop",
"native-title-bar": "Bară de titlu nativă",
"native-title-bar-description": "Pentru Windows și macOS, dezactivarea bării de titlu native face aplicația să pară mai compactă. Pe Linux, păstrarea bării integrează mai bine aplicația cu restul sistemului de operare.",
@@ -1781,7 +1776,8 @@
"geo-map": {
"create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă",
"unable-to-load-map": "Nu s-a putut încărca harta.",
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula."
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula.",
"create-child-note-text": "Adaugă marcaj"
},
"duration": {
"days": "zile",
@@ -2127,7 +2123,7 @@
},
"call_to_action": {
"background_effects_title": "Efectele de fundal sunt acum stabile",
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
"background_effects_message": "Pe dispozitive cu Windows și macOS, efectele de fundal sunt stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei.",
"background_effects_button": "Activează efectele de fundal",
"next_theme_title": "Încercați noua temă Trilium",
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
@@ -2281,5 +2277,14 @@
"pages_other": "{{count}} de pagini",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Încărcare..."
},
"platform_indicator": {
"available_on": "Disponibil pe {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} tab",
"title_few": "{{count}} taburi",
"title_other": "{{count}} de taburi",
"more_options": "Mai multe opțiuni"
}
}

View File

@@ -668,7 +668,8 @@
"geo-map": {
"unable-to-load-map": "Не удалось загрузить карту.",
"create-child-note-instruction": "Щелкните по карте, чтобы создать новую заметку в этом месте, или нажмите Escape, чтобы закрыть ее.",
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту"
"create-child-note-title": "Создать новую дочернюю заметку и добавить ее на карту",
"create-child-note-text": "Добавить маркер"
},
"note_tooltip": {
"quick-edit": "Быстрое редактирование",
@@ -685,8 +686,8 @@
"electron_integration": {
"zoom-factor": "Коэффициент масштабирования",
"restart-app-button": "Применить изменения и перезапустить приложение",
"background-effects-description": "Эффект Mica добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты (только Windows 11)",
"background-effects-description": "Добавляет размытый, стильный фон окнам приложений, создавая глубину и современный вид. Опция \"Системная строка заголовка\" должна быть отключена.",
"background-effects": "Включить фоновые эффекты",
"native-title-bar-description": "В Windows и macOS отключение системной строки заголовка делает приложение более компактным. В Linux включение системной строки заголовка улучшает интеграцию с остальной частью системы.",
"native-title-bar": "Системная панель заголовка",
"desktop-application": "Десктопное приложение"
@@ -776,7 +777,11 @@
"refresh-saved-search-results": "Обновить сохраненные результаты поиска",
"automatically-collapse-notes-title": "Заметки будут свернуты после определенного периода бездействия, чтобы навести порядок в дереве.",
"toggle-sidebar": "Переключить боковую панель",
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено."
"dropping-not-allowed": "Перетаскивание заметок в эту область не разрешено.",
"shared-indicator-tooltip": "Эта заметка опубликована",
"shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}",
"subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.",
"subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве."
},
"quick-search": {
"no-results": "Результаты не найдены",
@@ -856,7 +861,10 @@
"convert-to-attachment-confirm": "Вы уверены, что хотите преобразовать выбранные заметки во вложения их родительских заметок? Эта операция применяется только к заметкам в виде изображений; другие заметки будут пропущены.",
"converted-to-attachments": "{{count}} заметок были преобразованы во вложения.",
"archive": "Архивировать",
"unarchive": "Разархивировать"
"unarchive": "Разархивировать",
"open-in-a-new-window": "Открыть в новом окне",
"hide-subtree": "Скрыть поддерево",
"show-subtree": "Показать поддерево"
},
"info": {
"closeButton": "Закрыть",
@@ -1000,7 +1008,8 @@
"switch_to_mobile_version": "Перейти на мобильную версию",
"switch_to_desktop_version": "Переключиться на версию для ПК",
"new-version-available": "Доступно обновление",
"download-update": "Обновить до {{latestVersion}}"
"download-update": "Обновить до {{latestVersion}}",
"search_notes": "Поиск заметок"
},
"zpetne_odkazy": {
"relation": "отношение",
@@ -1047,7 +1056,8 @@
"expand_all_levels": "Развернуть все вложенные уровни",
"expand_nth_level": "Развернуть уровни: {{depth}} шт.",
"expand_first_level": "Развернуть прямые дочерние уровни",
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа."
"expand_tooltip": "Разщвернуть дочерние элементы этой коллекции (на один уровень вложенности). Для получения дополнительных параметров нажмите стрелку справа.",
"hide_child_notes": "Скрыть дочерние заметки в дереве"
},
"edited_notes": {
"deleted": "(удалено)",
@@ -1692,7 +1702,7 @@
"zoom_in_title": "Увеличить масштаб",
"zoom_out_title": "Уменьшить масштаб",
"reset_pan_zoom_title": "Сбросить панорамирование и масштабирование",
"create_child_note_title": "Создать новую дочернюю заметку и добавить ее в эту карту связей"
"create_child_note_title": "Создать дочернюю заметку и добавить ее в карту"
},
"code_auto_read_only_size": {
"unit": "символов",
@@ -1845,7 +1855,8 @@
"error_cannot_get_branch_id": "Невозможно получить branchId для notePath '{{notePath}}'",
"delete_this_note": "Удалить эту заметку",
"insert_child_note": "Вставить дочернюю заметку",
"note_revisions": "История изменений"
"note_revisions": "История изменений",
"content_language_switcher": "Язык содержимого: {{language}}"
},
"svg_export_button": {
"button_title": "Экспортировать диаграмму как SVG"
@@ -1900,7 +1911,7 @@
"dismiss": "Отклонить",
"background_effects_button": "Включить эффекты фона",
"next_theme_button": "Попробовать новую тему",
"background_effects_message": "На устройствах Windows фоновые эффекты теперь полностью стабильны. Они добавляют цвет в пользовательский интерфейс, размывая фон за ним. Этот приём также используется в других приложениях, например, в проводнике Windows.",
"background_effects_message": "На устройствах с ОС Windows или macOS, фоновые эффекты теперь полностью стабильны. Они добавляют цвета в пользовательский интерфейс, размывая фон за ним.",
"background_effects_title": "Фоновые эффекты теперь стабильны",
"next_theme_title": "Попробуйте новую тему Trilium",
"new_layout_button": "Подробнее",
@@ -1988,11 +1999,6 @@
"attachment_deleted": "Это вложение было удалено.",
"you_can_also_open": ", вы также можете открыть "
},
"web_view": {
"web_view": "Веб-страница",
"create_label": "Для начала создайте метку с URL-адресом, который вы хотите встроить, например, #webViewSrc=\"https://www.google.com\"",
"embed_websites": "Заметки типа \"Веб-страница\" позволяет встраивать веб-сайты в Trilium."
},
"ribbon": {
"widgets": "Виджеты ленты",
"promoted_attributes_message": "Вкладка \"Продвигаемые атрибуты\" будет автоматически открыта, если таковые атрибуты установлены у заметки",
@@ -2094,7 +2100,11 @@
"ui": "Пользовательский интерфейс"
},
"sql_result": {
"no_rows": "По этому запросу не возвращено ни одной строки"
"no_rows": "По этому запросу не возвращено ни одной строки",
"not_executed": "Запрос еще не выполнен.",
"failed": "Выполнение SQL-запроса завершилось с ошибкой",
"statement_result": "Результат заявления",
"execute_now": "Выполнить сейчас"
},
"editable_code": {
"placeholder": "Введите содержимое для заметки с кодом..."
@@ -2189,7 +2199,14 @@
"read_only_auto_description": "Эта заметка была автоматически переведена в режим только для чтения по соображениям производительности. Это автоматическое ограничение можно изменить в настройках.\n\nНажмите, чтобы временно отредактировать её.",
"read_only_auto": "Автоматический режим \"только для чтения\"",
"read_only_explicit_description": "Эта заметка была вручную установлена в режим «только для чтения».\nНажмите, чтобы временно отредактировать её.",
"read_only_explicit": "Только для чтения"
"read_only_explicit": "Только для чтения",
"save_status_saving": "Сохранение...",
"save_status_saved": "Сохранение",
"save_status_unsaved": "Не сохранено",
"save_status_error": "Ошибка сохранения",
"save_status_saving_tooltip": "Изменения сохраняются.",
"save_status_unsaved_tooltip": "Есть несохраненные изменения. Они будут сохранены автоматически через некоторое время.",
"save_status_error_tooltip": "Произошла ошибка при сохранении заметки. Если возможно, попробуйте скопировать содержимое заметки в другое место и перезагрузить приложение."
},
"breadcrumb": {
"hoisted_badge_title": "Снять фокус",
@@ -2243,5 +2260,30 @@
},
"attributes_panel": {
"title": "Атрибуты заметки"
},
"bookmark_buttons": {
"bookmarks": "Закладки"
},
"mobile_tab_switcher": {
"more_options": "Показать больше",
"title_one": "{{count}} вкладка",
"title_few": "{{count}} вкладки",
"title_many": "{{count}} вкладок"
},
"pdf": {
"pages_loading": "Загрузка...",
"pages_alt": "Страница {{pageNumber}}",
"pages_one": "{{count}} страница",
"pages_few": "{{count}} страницы",
"pages_many": "{{count}} страниц",
"layers_one": "{{count}} слой",
"layers_few": "{{count}} слоя",
"layers_many": "{{count}} слоев",
"attachments_one": "{{count}} вложение",
"attachments_few": "{{count}} вложения",
"attachments_many": "{{count}} вложений"
},
"platform_indicator": {
"available_on": "Доступно для {{platform}}"
}
}

View File

@@ -662,7 +662,8 @@
"show-cheatsheet": "顯示快捷鍵說明",
"toggle-zen-mode": "禪模式",
"new-version-available": "發現新更新",
"download-update": "取得版本 {{latestVersion}}"
"download-update": "取得版本 {{latestVersion}}",
"search_notes": "搜尋筆記"
},
"sync_status": {
"unknown": "<p>同步狀態將在下一次同步嘗試開始後顯示。</p><p>點擊以立即觸發同步。</p>",
@@ -757,7 +758,9 @@
"delete_this_note": "刪除此筆記",
"error_cannot_get_branch_id": "無法獲取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "無法識別的命令 {{command}}",
"note_revisions": "筆記歷史版本"
"note_revisions": "筆記歷史版本",
"backlinks": "反向連結",
"content_language_switcher": "內文語言:{{language}}"
},
"note_icon": {
"change_note_icon": "更改筆記圖標",
@@ -909,7 +912,8 @@
"unknown_search_option": "未知的搜尋選項 {{searchOptionName}}",
"search_note_saved": "搜尋筆記已儲存至 {{- notePathTitle}}",
"actions_executed": "已執行操作。",
"view_options": "查看選項:"
"view_options": "查看選項:",
"option": "選項"
},
"similar_notes": {
"title": "相似筆記",
@@ -1063,11 +1067,6 @@
"note_detail_render_help_1": "之所以顯示此說明筆記,是因為該類型的渲染 HTML 沒有設定好必須的關聯。",
"note_detail_render_help_2": "渲染筆記類型用於編寫 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">腳本</a>。簡單說就是您可以寫HTML程式碼或者加上一些JavaScript程式碼 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">關聯</a> 指向要呈現的 HTML 筆記。"
},
"web_view": {
"web_view": "網頁顯示",
"embed_websites": "網頁顯示類型的筆記允許您將網站嵌入至 Trilium 中。",
"create_label": "首先,請新增一個帶有您要嵌入的 URL 地址的標籤,例如 #webViewSrc=\"https://www.bing.com\""
},
"backend_log": {
"refresh": "重新整理"
},
@@ -1939,8 +1938,8 @@
"desktop-application": "桌面版應用程式",
"native-title-bar": "原生標題列",
"native-title-bar-description": "對於 Windows 和 macOS關閉原生標題列會讓應用程式看起來更緊湊。在 Linux 上,開啟原生標題列可以與系統的其他部分整合得更好。",
"background-effects": "啟用背景效果(僅適用於 Windows 11",
"background-effects-description": "Mica 效果為程式視窗新增模糊且時尚的背景,營造出深度感和現代化外觀。「原生標題列」必須被禁用。",
"background-effects": "啟用背景效果",
"background-effects-description": "為程式視窗新增模糊且時尚的背景,營造立體感和現代化外觀。「原生標題列」必須被禁用。",
"restart-app-button": "重新啟動應用程式以查看更改",
"zoom-factor": "縮放係數"
},
@@ -1959,7 +1958,8 @@
"geo-map": {
"create-child-note-title": "建立一個新的子筆記並將其加至地圖中",
"create-child-note-instruction": "點擊地圖以在該位置建立新筆記,或按 Escape 以取消。",
"unable-to-load-map": "無法載入地圖。"
"unable-to-load-map": "無法載入地圖。",
"create-child-note-text": "新增地圖標記"
},
"geo-map-context": {
"open-location": "打開位置",
@@ -2122,7 +2122,7 @@
},
"call_to_action": {
"background_effects_title": "背景特效已推出穩定版本",
"background_effects_message": "在 Windows 裝置上,背景特效現在已完全穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。此技術也用於其他應用程式,例如 Windows 檔案總管。",
"background_effects_message": "在 Windows 和macOS裝置上,背景特效現在已穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。",
"background_effects_button": "啟用背景特效",
"next_theme_title": "試用新 Trilium 主題",
"next_theme_message": "您正在使用舊版主題,要試用新主題嗎?",
@@ -2267,5 +2267,16 @@
"pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "正在載入…"
},
"mobile_tab_switcher": {
"more_options": "更多選項",
"title_one": "{{count}} 個分頁",
"title_other": ""
},
"platform_indicator": {
"available_on": "可於 {{platform}} 使用"
},
"bookmark_buttons": {
"bookmarks": "書籤"
}
}

View File

@@ -1134,11 +1134,6 @@
"note_detail_render_help_1": "Ця довідка відображається, оскільки ця нотатка типу Render HTML не має необхідного зв'язку для належного функціонування.",
"note_detail_render_help_2": "Тип нотатки Render HTML використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптів</a>. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">відношення</a> під назвою \"renderNote\", яке вказує на нотатку HTML для відображення."
},
"web_view": {
"web_view": "Веб-перегляд",
"embed_websites": "Нотатка типу Веб-перегляд дозволяє вбудовувати веб-сайти в Trilium.",
"create_label": "Для початку створіть мітку з URL-адресою, яку ви хочете вбудувати, наприклад, #webViewSrc=\"https://www.google.com\""
},
"backend_log": {
"refresh": "Оновити"
},

View File

@@ -1,3 +1,5 @@
export {};
// TODO: Use real @types/ but that one generates a lot of errors.
declare module "draggabilly" {
type DraggabillyEventData = {};
@@ -32,26 +34,6 @@ declare module "katex/contrib/auto-render" {
export default renderMathInElement;
}
import * as L from "leaflet";
declare module "leaflet" {
interface GPXMarker {
startIcon?: DivIcon | Icon | string | undefined;
endIcon?: DivIcon | Icon | string | undefined;
wptIcons?: {
[key: string]: DivIcon | Icon | string;
};
wptTypeIcons?: {
[key: string]: DivIcon | Icon | string;
};
pointMatchers?: Array<{ regex: RegExp; icon: DivIcon | Icon | string}>;
}
interface GPXOptions {
markers?: GPXMarker | undefined;
}
}
declare global {
interface Navigator {
/** Returns a boolean indicating whether the browser is running in standalone mode. Available on Apple's iOS Safari only. */

View File

@@ -0,0 +1,83 @@
.tn-backlinks-widget .backlinks-items {
list-style-type: none;
margin: 0;
padding: 0;
position: static;
width: unset;
> li {
--border-radius: 8px;
max-width: 600px;
padding: 10px 20px;
background: var(--card-background-color);
& + li {
margin-top: 2px;
}
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
> span {
display: flex;
flex-wrap: wrap;
align-items: center;
/* Note path */
> small {
flex: 100%;
order: -1;
font-size: .65rem;
.note-path {
padding: 0;
}
}
/* Note icon */
> .tn-icon {
color: var(--menu-item-icon-color);
}
/* Note title */
> a {
margin-inline-start: 4px;
color: currentColor;
font-weight: 500;
}
}
}
/* Card content - excerpt */
.backlink-excerpt {
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
display: block;
margin: 8px 0;
border-radius: 4px;
background: var(--quick-search-result-content-background);
padding: 8px;
font-size: .75rem;
a {
background: transparent;
color: var(--quick-search-result-highlight-color);
text-decoration: underline;
}
p {
margin: 0;
}
}
}
}

View File

@@ -88,12 +88,6 @@
}
/* #endregion */
/* #region Geo map buttons */
.leaflet-pane {
z-index: 50;
}
/* #endregion */
/* #region Close floating buttons */
.close-floating-buttons {
margin-inline-start: 5px !important;

View File

@@ -1,3 +1,5 @@
import "./Backlinks.css";
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import { VNode } from "preact";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
@@ -60,14 +62,6 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
Backlinks
];
export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
RefreshBackendLogButton,
EditButton,
RelationMapButtons,
ExportImageButtons,
Backlinks
];
/**
* Floating buttons that should be hidden in popup editor (Quick edit).
*/

View File

@@ -3,6 +3,30 @@
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
contain: none;
&.fixed-tree {
display: flex;
flex-direction: column;
height: 100%;
.fixed-note-tree-container {
height: 60%;
border-bottom: 1px solid var(--main-border-color);
overflow: auto;
.tree-wrapper {
padding: 0;
}
.tree {
padding: 0;
}
ul {
margin: 0;
}
}
}
}
body.prefers-centered-content .note-detail {
@@ -12,4 +36,4 @@ body.prefers-centered-content .note-detail {
.note-detail > * {
contain: none;
}
}

View File

@@ -1,5 +1,7 @@
import "./NoteDetail.css";
import clsx from "clsx";
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
import { isValidElement, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -12,8 +14,9 @@ import { t } from "../services/i18n";
import protected_session_holder from "../services/protected_session_holder";
import toast from "../services/toast.js";
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
import NoteTreeWidget from "./note_tree";
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
import { useNoteContext, useTriliumEvent } from "./react/hooks";
import { useLegacyWidget, useNoteContext, useTriliumEvent } from "./react/hooks";
import { NoteListWithLinks } from "./react/NoteList";
import { TypeWidgetProps } from "./type_widgets/type_widget";
@@ -36,6 +39,7 @@ export default function NoteDetail() {
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
const widgetRequestId = useRef(0);
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
const props: TypeWidgetProps = {
note: note!,
@@ -119,13 +123,6 @@ export default function NoteDetail() {
}
});
// Fixed tree for launch bar config on mobile.
useEffect(() => {
if (!isMobile) return;
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot";
document.body.classList.toggle("force-fixed-tree", hasFixedTree);
}, [ note ]);
// Handle toast notifications.
useEffect(() => {
if (!isElectron()) return;
@@ -215,8 +212,13 @@ export default function NoteDetail() {
return (
<div
ref={containerRef}
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
class={clsx("component note-detail", {
"full-height": isFullHeight,
"fixed-tree": hasFixedTree
})}
>
{hasFixedTree && <FixedTree noteContext={noteContext} />}
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper
Element={Element}
@@ -231,6 +233,11 @@ export default function NoteDetail() {
);
}
function FixedTree({ noteContext }: { noteContext: NoteContext }) {
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
return <div class="fixed-note-tree-container">{treeEl}</div>;
}
/**
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.
@@ -349,6 +356,14 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte
// https://github.com/zadam/trilium/issues/2522
const isBackendNote = noteContext?.noteId === "_backendLog";
const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight;
// Allow vertical centering when there are no results.
if (type === "book" &&
[ "grid", "list" ].includes(noteContext.note?.getLabelValue("viewType") ?? "grid") &&
!noteContext.note?.hasChildren()) {
return true;
}
return (!noteContext?.hasNoteList() && isFullHeightNoteType)
|| noteContext?.viewScope?.viewMode === "attachments"
|| isBackendNote;
@@ -364,7 +379,33 @@ function showToast(type: "printing" | "exporting_pdf", progress: number = 0) {
}
function handlePrintReport(printReport?: PrintReport) {
if (printReport?.type === "collection" && printReport.ignoredNoteIds.length > 0) {
if (!printReport) return;
if (printReport.type === "error") {
toast.showPersistent({
id: "print-error",
icon: "bx bx-error-circle",
title: t("note_detail.print_report_error_title"),
message: printReport.message,
buttons: printReport.stack ? [
{
text: t("note_detail.print_report_collection_details_button"),
onClick(api) {
api.dismissToast();
dialog.info(<>
<p>{printReport.message}</p>
<details>
<summary>{t("note_detail.print_report_stack_trace")}</summary>
<pre style="font-size: 0.85em; overflow-x: auto;">{printReport.stack}</pre>
</details>
</>, {
title: t("note_detail.print_report_error_title")
});
}
}
] : undefined
});
} else if (printReport.type === "collection" && printReport.ignoredNoteIds.length > 0) {
toast.showPersistent({
id: "print-report",
icon: "bx bx-collection",

View File

@@ -16,6 +16,10 @@ body.mobile .promoted-attributes-widget {
display: table;
}
body.experimental-feature-new-layout .promoted-attributes-container {
max-height: unset;
}
.promoted-attribute-cell {
display: flex;
align-items: center;
@@ -94,4 +98,4 @@ body.mobile .promoted-attributes-widget {
background: rgba(0, 0, 0, 0.5);
transform: rotate(45deg);
pointer-events: none;
}
}

View File

@@ -5,6 +5,7 @@ import clsx from "clsx";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import FAttribute from "../entities/fattribute";
import FNote from "../entities/fnote";
import { Attribute } from "../services/attribute_parser";
@@ -40,8 +41,8 @@ type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | J
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
export default function PromotedAttributes() {
const { note, componentId } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
const { note, componentId, noteContext } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
}
@@ -74,12 +75,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
*
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
*/
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
const [ viewType ] = useNoteLabel(note, "viewType");
const [ cells, setCells ] = useState<Cell[]>();
function refresh() {
if (!note || viewType === "table") {
if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") {
setCells([]);
return;
}
@@ -124,7 +125,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone
setCells(cells);
}
useEffect(refresh, [ note, viewType ]);
useEffect(refresh, [ note, viewType, noteContext ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
refresh();

View File

@@ -29,7 +29,6 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
const isVerticalLayout = !isHorizontalLayout;
const parentComponent = useContext(ParentComponent);
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
const isMobileLocal = isMobile();
const logoRef = useRef<SVGSVGElement>(null);
useStaticTooltip(logoRef);
@@ -44,9 +43,12 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
</div>}
</>}
noDropdownListStyle
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
mobileBackdrop
>
{isMobile() && <>
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
<FormDropdownDivider />
</>}
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
@@ -107,8 +109,7 @@ function BrowserOnlyOptions() {
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
return <>
<FormDropdownDivider />
<FormListItem disabled>Development Options</FormListItem>
<FormListHeader text="Development Options" />
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
{experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />

View File

@@ -2,8 +2,12 @@
min-height: 0;
max-width: var(--max-content-width); /* Inherited from .note-split */
overflow: auto;
overflow: visible;
contain: none !important;
&.full-height {
overflow: auto;
}
}
body.prefers-centered-content .note-list-widget:not(.full-height) {

View File

@@ -10,12 +10,6 @@
--card-padding: 0.6em;
}
body.mobile .board-view {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container {
height: 100%;
display: flex;
@@ -26,6 +20,12 @@ body.mobile .board-view {
overflow-x: auto;
}
body.mobile .board-view-container {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container .board-column {
width: 250px;
flex-shrink: 0;

View File

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

View File

@@ -1,14 +1,51 @@
import type { LatLng, LeafletMouseEvent } from "leaflet";
import appContext, { type CommandMappings } from "../../../components/app_context.js";
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
import linkContextMenu from "../../../menus/link_context_menu.js";
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
import { t } from "../../../services/i18n.js";
import { createNewNote } from "./api.js";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import link from "../../../services/link.js";
import { MapMouseEvent } from "maplibre-gl";
import { useCallback, useContext, useEffect } from "preact/hooks";
export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
import appContext, { type CommandMappings } from "../../../components/app_context.js";
import FNote from "../../../entities/fnote.js";
import contextMenu, { type MenuItem } from "../../../menus/context_menu.js";
import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx";
import linkContextMenu from "../../../menus/link_context_menu.js";
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
import { t } from "../../../services/i18n.js";
import link from "../../../services/link.js";
import { createNewNote } from "./api.js";
import { type GeoMouseEvent,ParentMap, toMapLibreEvent } from "./map.js";
import { MARKER_LAYER } from "./Markers.js";
export default function ContextMenus({ note, isReadOnly }: { note: FNote, isReadOnly }) {
const map = useContext(ParentMap);
const onContextMenu = useCallback((e: GeoMouseEvent) => {
if (!map) return;
const features = map.queryRenderedFeatures(e.point, {
layers: [ MARKER_LAYER ]
});
if (features.length > 0) {
// Marker context menu.
openContextMenu(features[0].properties.id, e, !isReadOnly);
} else {
// Empty area context menu.
openMapContextMenu(note.noteId, e, !isReadOnly);
}
}, [ map, note.noteId, isReadOnly ]);
useEffect(() => {
if (!onContextMenu || !map) return;
const handler = (e: MapMouseEvent) => {
e.preventDefault();
onContextMenu(toMapLibreEvent(e));
};
map.on("contextmenu", handler);
return () => { map.off("contextmenu", handler); };
}, [ map, onContextMenu ]);
return null;
}
export function openContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e),
{ kind: "separator" },
@@ -44,7 +81,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is
});
}
export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) {
export function openMapContextMenu(noteId: string, e: GeoMouseEvent, isEditable: boolean) {
let items: MenuItem<keyof CommandMappings>[] = [
...buildGeoLocationItem(e)
];
@@ -58,7 +95,7 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita
handler: () => createNewNote(noteId, e),
uiIcon: "bx bx-plus"
}
]
];
}
contextMenu.show({
@@ -71,8 +108,8 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita
});
}
function buildGeoLocationItem(e: LeafletMouseEvent) {
function formatGeoLocation(latlng: LatLng, precision: number = 6) {
function buildGeoLocationItem(e: GeoMouseEvent) {
function formatGeoLocation(latlng: { lat: number; lng: number }, precision: number = 6) {
return `${latlng.lat.toFixed(precision)}, ${latlng.lng.toFixed(precision)}`;
}

View File

@@ -0,0 +1,250 @@
import { AddLayerObject, type GeoJSONSource } from "maplibre-gl";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { useChildNotes } from "../../react/hooks";
import { ParentMap } from "./map";
export const LOCATION_ATTRIBUTE = "geolocation";
export const MARKER_LAYER = "points-layer";
const MARKER_SOURCE = "points";
const DEFAULT_MARKER_COLOR = "#2A81CB";
// SVG marker pin shape (replaces the Leaflet marker PNG).
export const MARKER_SVG = "foo"; // TODO: Fix
const iconSvgCache = new Map<string, string>();
const iconClassToBitmapCache = new Map<string, string | undefined>();
const LABEL_LAYOUT: Extract<AddLayerObject, { type: "symbol"}>["layout"] = {
"text-field": ["get", "name"],
"text-font": ["Open Sans Regular"],
"text-size": 12,
"text-anchor": "top",
"text-allow-overlap": true,
};
const MARKER_WIDTH = 25;
const MARKER_ICON_SIZE = 20;
const MARKER_ICON_X = (MARKER_WIDTH / 2) - (MARKER_ICON_SIZE / 2);
export default function Markers({ note, hideLabels }: { note: FNote, hideLabels: boolean }) {
const map = useContext(ParentMap);
const childNotes = useChildNotes(note?.noteId);
// Add the source and layer.
useEffect(() => {
if (!map) return;
// Wait for styles to be loaded.
map.on("style.load", () => {
map.addSource(MARKER_SOURCE, {
type: "geojson",
data: {
type: "FeatureCollection",
features: []
}
});
map.addLayer({
id: MARKER_LAYER,
type: "symbol",
source: MARKER_SOURCE,
layout: {
"icon-image": [ "get", "icon" ],
"icon-size": 1,
"icon-anchor": "bottom",
"icon-allow-overlap": true,
...hideLabels ? [] : LABEL_LAYOUT
},
paint: {
"text-color": "#333",
"text-halo-color": "#fff",
"text-halo-width": 1,
}
});
});
return () => {
if (map.getLayer(MARKER_LAYER)) {
map.removeLayer(MARKER_LAYER);
}
if (map.getSource(MARKER_SOURCE)) {
map.removeSource(MARKER_SOURCE);
}
};
}, [ map ]);
const refresh = useCallback(async () => {
if (!map) return;
async function ensureIcon(color: string, iconClass: string) {
const key = `marker-${color}-${iconClass}`;
if (!iconSvgCache.has(key)) {
const svg = await buildMarkerIcon(color, iconClass);
iconSvgCache.set(key, svg);
}
return key;
}
const features: GeoJSON.Feature[] = [];
for (const childNote of childNotes) {
const location = childNote.getLabelValue(LOCATION_ATTRIBUTE);
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
if (!latLng) continue;
latLng.reverse();
const color = childNote.getLabelValue("color") ?? DEFAULT_MARKER_COLOR;
features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: latLng
},
properties: {
id: childNote.noteId,
name: childNote.title,
icon: await ensureIcon(color, childNote.getIcon())
}
});
}
// Build all the icons.
await Promise.all(iconSvgCache.entries().map(async ([ key, svg ]) => {
const image = await svgToImage(svg);
map.addImage(key, image, {
pixelRatio: window.devicePixelRatio
});
}));
// Update the source
const source = map.getSource<GeoJSONSource>(MARKER_SOURCE);
source?.setData({
type: "FeatureCollection",
features
});
}, [ childNotes, map ]);
// Refresh the data.
useEffect(() => {
refresh();
}, [ refresh ]);
return null;
}
//#region Renderer
async function buildMarkerIcon(color: string, iconClass: string, scale = window.devicePixelRatio || 1) {
const iconUrl = iconClassToBitmapCache.get(iconClass) ?? await snapshotIcon(iconClass, MARKER_ICON_SIZE * scale);
return `\
<svg width="${MARKER_WIDTH * scale}" height="${41 * scale}" viewBox="0 0 ${MARKER_WIDTH} 41" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="black" flood-opacity="0.4" />
</filter>
</defs>
<path d="M12.5 0C5.6 0 0 5.6 0 12.5C0 21.9 12.5 41 12.5 41S25 21.9 25 12.5C25 5.6 19.4 0 12.5 0Z" filter="url(#shadow)" fill="${color}" />
<image href="${iconUrl}" x="${MARKER_ICON_X}" y="4" width="${MARKER_ICON_SIZE}" height="${MARKER_ICON_SIZE}" preserveAspectRatio="xMidYMid meet" />
</svg>
`;
}
async function snapshotIcon(iconClass: string, size: number) {
const cachedIcon = iconClassToBitmapCache.get(iconClass);
if (cachedIcon) return cachedIcon;
await document.fonts.ready;
const glyph = getGlyphFromClass(iconClass);
const rendered = renderMarkerCanvas({
color: "black",
glyph,
size
});
const dataUrl = rendered?.toDataURL();
iconClassToBitmapCache.set(iconClass, dataUrl);
return dataUrl;
}
function renderMarkerCanvas({
color,
glyph, // e.g. "\uf123"
size = 32,
scale = window.devicePixelRatio || 1
}) {
const canvas = document.createElement("canvas");
// High-DPI canvas
canvas.width = size * scale;
canvas.height = size * scale;
const ctx = canvas.getContext("2d");
if (!ctx) return null;
// Scale for retina
ctx.scale(scale, scale);
ctx.clearRect(0, 0, size, size);
// Set font
ctx.font = `${size}px ${glyph.fontFamily}`;
ctx.fillStyle = color;
// Measure glyph
const metrics = ctx.measureText(glyph.content);
const glyphWidth =
metrics.actualBoundingBoxLeft +
metrics.actualBoundingBoxRight;
const glyphHeight =
metrics.actualBoundingBoxAscent +
metrics.actualBoundingBoxDescent;
// Center position
const x = (size - glyphWidth) / 2 + metrics.actualBoundingBoxLeft;
const y = (size - glyphHeight) / 2 + metrics.actualBoundingBoxAscent;
// Draw
ctx.fillText(glyph.content, x, y);
return canvas;
}
function getGlyphFromClass(iconClass: string) {
const el = document.createElement("span");
el.className = iconClass;
document.body.appendChild(el);
const style = window.getComputedStyle(el, "::before");
const content = style.getPropertyValue("content");
const fontFamily = style.getPropertyValue("font-family");
document.body.removeChild(el);
if (!content || content === "none") {
return null;
}
// content is usually quoted like: '"\f123"'
return {
fontFamily,
content: content.replace(/^["']|["']$/g, "")
};
}
function svgToImage(svgString: string){
return new Promise<HTMLImageElement>(resolve => {
const svgBlob = new Blob([svgString], { type: "image/svg+xml" });
const url = URL.createObjectURL(svgBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
resolve(img);
};
img.src = url;
});
}
//#endregion

View File

@@ -0,0 +1,44 @@
import { type MapGeoJSONFeature, MapMouseEvent, Popup } from "maplibre-gl";
import { useContext, useEffect } from "preact/hooks";
import { ParentMap } from "./map";
import { MARKER_LAYER } from "./Markers";
export default function Tooltips() {
const map = useContext(ParentMap);
useEffect(() => {
if (!map) return;
const tooltip = new Popup({
closeButton: false,
closeOnClick: false,
offset: 12,
className: "marker-tooltip"
});
function onMouseEnter(e: MapMouseEvent & { features?: MapGeoJSONFeature[]; }) {
const feature = e.features?.[0];
if (!feature || !map || feature.geometry.type !== "Point") return;
tooltip
.setLngLat(feature.geometry.coordinates as [ number, number ])
.setHTML(`<strong>${feature.properties.name}</strong>`)
.addTo(map);
}
function onMouseLeave() {
tooltip.remove();
}
map.on("mouseenter", MARKER_LAYER, onMouseEnter);
map.on("mouseleave", MARKER_LAYER, onMouseLeave);
return () => {
map.off("mouseenter", MARKER_LAYER, onMouseEnter);
map.off("mouseleave", MARKER_LAYER, onMouseLeave);
};
}, [ map ]);
return null;
}

View File

@@ -1,19 +1,20 @@
import type { LatLng, LeafletMouseEvent } from "leaflet";
import { LOCATION_ATTRIBUTE } from ".";
import { CreateChildrenResponse } from "@triliumnext/commons";
import attributes from "../../../services/attributes";
import { prompt } from "../../../services/dialog";
import server from "../../../services/server";
import { t } from "../../../services/i18n";
import { CreateChildrenResponse } from "@triliumnext/commons";
import server from "../../../services/server";
import type { GeoMouseEvent } from "./map";
import { LOCATION_ATTRIBUTE } from "./Markers";
const CHILD_NOTE_ICON = "bx bx-pin";
export async function moveMarker(noteId: string, latLng: LatLng | null) {
export async function moveMarker(noteId: string, latLng: { lat: number; lng: number } | null) {
const value = latLng ? [latLng.lat, latLng.lng].join(",") : "";
await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value);
}
export async function createNewNote(noteId: string, e: LeafletMouseEvent) {
export async function createNewNote(noteId: string, e: GeoMouseEvent) {
const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") });
if (title?.trim()) {

View File

@@ -7,50 +7,41 @@
> .collection-properties {
position: relative;
z-index: 2000;
}
}
.geo-map-container {
height: 100%;
overflow: hidden;
}
.leaflet-pane {
z-index: 1;
}
.leaflet-top,
.leaflet-bottom {
z-index: 997;
.maplibregl-canvas-container {
position: relative;
}
}
.geo-view.placing-note .geo-map-container {
cursor: crosshair;
}
.geo-map-container .marker-pin {
.geo-map-container .geo-marker {
position: relative;
}
.geo-map-container .leaflet-div-icon {
position: relative;
background: transparent;
border: 0;
cursor: pointer;
overflow: visible;
}
.geo-map-container .leaflet-div-icon .icon-shadow {
position: absolute;
top: 0;
inset-inline-start: 0;
z-index: -1;
.geo-map-container .geo-marker .marker-pin {
position: relative;
}
.geo-map-container .leaflet-div-icon .tn-icon {
.geo-map-container .geo-marker .marker-pin svg {
display: block;
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.3));
}
.geo-map-container .geo-marker .tn-icon {
position: absolute;
top: 3px;
inset-inline-start: 2px;
inset-inline-start: 4px;
background-color: white;
color: var(--light-theme-custom-color, black);
padding: 2px;
@@ -58,7 +49,7 @@
font-size: 17px;
}
.geo-map-container .leaflet-div-icon .title-label {
.geo-map-container .geo-marker .title-label {
display: block;
position: absolute;
top: 100%;
@@ -71,19 +62,19 @@
text-align: center;
text-overflow: ellipsis;
text-shadow: -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 1px 0 white;
white-space: no-wrap;
white-space: nowrap;
overflow: hidden;
}
body[dir=rtl] .geo-map-container .leaflet-div-icon .title-label {
body[dir=rtl] .geo-map-container .geo-marker .title-label {
transform: translateX(50%);
}
.geo-map-container .leaflet-div-icon .archived {
.geo-map-container .geo-marker .archived {
opacity: 0.5;
}
.geo-map-container.dark .leaflet-div-icon .title-label {
.geo-map-container.dark .geo-marker .title-label {
color: white;
text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black;
}

View File

@@ -1,8 +1,6 @@
import "./index.css";
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import type { Map as MapLibreGL } from "maplibre-gl";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
@@ -15,23 +13,24 @@ import toast from "../../../services/toast";
import CollectionProperties from "../../note_bars/CollectionProperties";
import ActionButton from "../../react/ActionButton";
import { ButtonOrActionButton } from "../../react/Button";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTooltip, useTriliumEvent } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import ContextMenus from "./ContextMenus";
import Map, { GeoMouseEvent } from "./map";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
import Markers, { LOCATION_ATTRIBUTE } from "./Markers";
import Tooltips from "./Tooltips";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2;
export const LOCATION_ATTRIBUTE = "geolocation";
interface MapData {
view?: {
center?: LatLng | [number, number];
center?: { lat: number; lng: number } | [number, number];
zoom?: number;
};
}
@@ -45,10 +44,11 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
const [ state, setState ] = useState(State.Normal);
const [ coordinates, setCoordinates ] = useState(viewConfig?.view?.center);
const [ zoom, setZoom ] = useState(viewConfig?.view?.zoom);
const [ layerName ] = useNoteLabel(note, "map:style");
const [ hasScale ] = useNoteLabelBoolean(note, "map:scale");
const [ hideLabels ] = useNoteLabelBoolean(note, "map:hideLabels");
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ notes, setNotes ] = useState<FNote[]>([]);
const layerData = useLayerData(note);
const spacedUpdate = useSpacedUpdate(() => {
if (viewConfig) {
saveConfig(viewConfig);
@@ -89,7 +89,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
moveMarker(noteId, null);
});
const onClick = useCallback(async (e: LeafletMouseEvent) => {
const onClick = useCallback(async (e: GeoMouseEvent) => {
if (state === State.NewNote) {
toast.closePersistent("geo-new-note");
await createNewNote(note.noteId, e);
@@ -97,13 +97,9 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
}
}, [ state ]);
const onContextMenu = useCallback((e: LeafletMouseEvent) => {
openMapContextMenu(note.noteId, e, !isReadOnly);
}, [ note.noteId, isReadOnly ]);
// Dragging
const containerRef = useRef<HTMLDivElement>(null);
const apiRef = useRef<L.Map>(null);
const apiRef = useRef<MapLibreGL | null>(null);
useNoteTreeDrag(containerRef, {
dragEnabled: !isReadOnly,
dragNotEnabledMessage: {
@@ -120,15 +116,15 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
const offset = containerRef.current?.getBoundingClientRect();
const x = e.clientX - (offset?.left ?? 0);
const y = e.clientY - (offset?.top ?? 0);
const latlng = api.containerPointToLatLng([ x, y ]);
const lngLat = api.unproject([x, y]);
const targetNote = await froca.getNote(noteId, true);
const parents = targetNote?.getParentNoteIds();
if (parents?.includes(note.noteId)) {
await moveMarker(noteId, latlng);
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
} else {
await branches.cloneNoteToParentNote(noteId, noteId);
await moveMarker(noteId, latlng);
await moveMarker(noteId, { lat: lngLat.lat, lng: lngLat.lng });
}
}
});
@@ -152,23 +148,46 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
zoom={zoom}
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
layerData={layerData}
viewportChanged={(coordinates, zoom) => {
if (!viewConfig) viewConfig = {};
viewConfig.view = { center: coordinates, zoom };
spacedUpdate.scheduleUpdate();
}}
onClick={onClick}
onContextMenu={onContextMenu}
scale={hasScale}
>
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
<Tooltips />
<ContextMenus note={note} isReadOnly={isReadOnly} />
<Markers note={note} hideLabels={hideLabels} />
</Map>}
<GeoMapTouchBar state={state} map={apiRef.current} />
</div>
);
}
function useLayerData(note: FNote) {
const [ layerName ] = useNoteLabel(note, "map:style");
// Memo is needed because it would generate unnecessary reloads due to layer change.
const layerData = useMemo(() => {
// Custom layers.
if (layerName?.startsWith("http")) {
return {
name: "Custom",
type: "raster",
url: layerName,
attribution: ""
} satisfies MapLayer;
}
// Built-in layers.
const layerData = MAP_LAYERS[layerName ?? ""] ?? MAP_LAYERS[DEFAULT_MAP_LAYER_NAME];
return layerData;
}, [ layerName ]);
return layerData;
}
function ToggleReadOnlyButton({ note }: { note: FNote }) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
@@ -179,31 +198,36 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) {
/>;
}
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
function NoteWrapper({ note, isReadOnly, hideLabels }: {
note: FNote,
isReadOnly: boolean,
hideLabels: boolean
}) {
const mime = useNoteProperty(note, "mime");
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
if (mime === "application/gpx+xml") {
return <NoteGpxTrack note={note} />;
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
}
if (location) {
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
if (!latLng) return;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} />;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
}
}
function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) {
function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) {
// React to changes
const [ color ] = useNoteLabel(note, "color");
const [ iconClass ] = useNoteLabel(note, "iconClass");
const [ archived ] = useNoteLabelBoolean(note, "archived");
const title = useNoteProperty(note, "title");
const icon = useMemo(() => {
return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived);
}, [ iconClass, color, title, note.noteId, archived]);
const iconHtml = useMemo(() => {
const titleOrNone = hideLabels ? undefined : title;
return buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, titleOrNone, note.noteId, archived);
}, [ note, iconClass, color, title, archived, hideLabels ]);
const onClick = useCallback(() => {
appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId });
@@ -218,15 +242,17 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
}
}, [ note.noteId ]);
const onDragged = useCallback((newCoordinates: LatLng) => {
const onDragged = useCallback((newCoordinates: { lat: number; lng: number }) => {
moveMarker(note.noteId, newCoordinates);
}, [ note.noteId ]);
const onContextMenu = useCallback((e: LeafletMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
const onContextMenu = useCallback((e: GeoMouseEvent) => openContextMenu(note.noteId, e, editable), [ note.noteId, editable ]);
return latLng && <Marker
coordinates={latLng}
icon={icon}
iconHtml={iconHtml}
iconSize={[25, 41]}
iconAnchor={[12, 41]}
draggable={editable}
onMouseDown={onMouseDown}
onDragged={editable ? onDragged : undefined}
@@ -235,7 +261,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
/>;
}
function NoteGpxTrack({ note }: { note: FNote }) {
function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) {
const [ xmlString, setXmlString ] = useState<string>();
const blob = useNoteBlob(note);
@@ -254,40 +280,34 @@ function NoteGpxTrack({ note }: { note: FNote }) {
const color = useNoteLabel(note, "color");
const iconClass = useNoteLabel(note, "iconClass");
const options = useMemo<GPXOptions>(() => ({
markers: {
startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title),
endIcon: buildIcon("bxs-flag-checkered"),
wptIcons: {
"": buildIcon("bx bx-pin")
}
},
polyline_options: {
color: note.getLabelValue("color") ?? "blue"
}
}), [ color, iconClass ]);
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
const trackColor = useMemo(() => note.getLabelValue("color") ?? "blue", [ color ]);
const startIconHtml = useMemo(() => buildIconHtml(note.getIcon(), note.getColorClass() ?? undefined, note.title), [ iconClass, color ]);
const endIconHtml = useMemo(() => buildIconHtml("bxs-flag-checkered"), [ ]);
const waypointIconHtml = useMemo(() => buildIconHtml("bx bx-pin"), [ ]);
return xmlString && <GpxTrack
gpxXmlString={xmlString}
trackColor={trackColor}
startIconHtml={startIconHtml}
endIconHtml={endIconHtml}
waypointIconHtml={waypointIconHtml}
/>;
}
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
function buildIconHtml(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
let html = /*html*/`\
<img class="icon" src="${markerIcon}" />
<img class="icon-shadow" src="${markerIconShadow}" />
<span class="bx ${bxIconClass} ${colorClass ?? ""}"></span>
<div class="marker-pin">${MARKER_SVG}</div>
<span class="bx ${bxIconClass} tn-icon ${colorClass ?? ""}"></span>
<span class="title-label">${title ?? ""}</span>`;
if (noteIdLink) {
html = `<div data-href="#root/${noteIdLink}" class="${archived ? "archived" : ""}">${html}</div>`;
}
return divIcon({
html,
iconSize: [25, 41],
iconAnchor: [12, 41]
});
return html;
}
function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | undefined }) {
function GeoMapTouchBar({ state, map }: { state: State, map: MapLibreGL | null | undefined }) {
const [ currentZoom, setCurrentZoom ] = useState<number>();
const parentComponent = useContext(ParentComponent);
@@ -299,7 +319,7 @@ function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | unde
}
map.on("zoom", onZoomChanged);
return () => map.off("zoom", onZoomChanged);
return () => { map.off("zoom", onZoomChanged); };
}, [ map ]);
return map && currentZoom && (

View File

@@ -1,147 +1,208 @@
import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks";
import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { MAP_LAYERS } from "./map_layer";
import { ComponentChildren, createContext, RefObject } from "preact";
import { useElementSize, useSyncedRef } from "../../react/hooks";
import "maplibre-gl/dist/maplibre-gl.css";
export const ParentMap = createContext<L.Map | null>(null);
import { Map as MapLibreGLMap, MapMouseEvent, NavigationControl, type Point, ScaleControl, StyleSpecification } from "maplibre-gl";
import { ComponentChildren, createContext, RefObject } from "preact";
import { useEffect, useImperativeHandle, useState } from "preact/hooks";
import { useElementSize, useSyncedRef } from "../../react/hooks";
import { MapLayer } from "./map_layer";
export interface GeoMouseEvent {
latlng: { lat: number; lng: number };
originalEvent: MouseEvent;
point: Point;
}
export const ParentMap = createContext<MapLibreGLMap | null>(null);
interface MapProps {
apiRef?: RefObject<L.Map | null>;
apiRef?: RefObject<MapLibreGLMap | null>;
containerRef?: RefObject<HTMLDivElement>;
coordinates: LatLng | [number, number];
coordinates: { lat: number; lng: number } | [number, number];
zoom: number;
layerName: string;
viewportChanged: (coordinates: LatLng, zoom: number) => void;
layerData: MapLayer;
viewportChanged: (coordinates: { lat: number; lng: number }, zoom: number) => void;
children: ComponentChildren;
onClick?: (e: LeafletMouseEvent) => void;
onContextMenu?: (e: LeafletMouseEvent) => void;
onClick?: (e: GeoMouseEvent) => void;
onContextMenu?: (e: GeoMouseEvent) => void;
onZoom?: () => void;
scale: boolean;
}
export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
const mapRef = useRef<L.Map>(null);
export function toMapLibreEvent(e: MapMouseEvent): GeoMouseEvent {
return {
latlng: { lat: e.lngLat.lat, lng: e.lngLat.lng },
originalEvent: e.originalEvent,
point: e.point
};
}
export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) {
const [ map, setMap ] = useState<MapLibreGLMap | null>(null);
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
useImperativeHandle(apiRef ?? null, () => mapRef.current);
useImperativeHandle(apiRef ?? null, () => map);
// Initialize the map.
useEffect(() => {
if (!containerRef.current) return;
const mapInstance = L.map(containerRef.current, {
worldCopyJump: false,
maxBounds: [
[-90, -180],
[90, 180]
],
minZoom: 2
let style: StyleSpecification | string;
if (layerData.type === "vector") {
style = typeof layerData.style === "string"
? layerData.style
: layerData.styleFallback;
} else {
style = {
version: 8,
sources: {
"raster-tiles": {
type: "raster",
tiles: [layerData.url],
tileSize: 256,
attribution: layerData.attribution
}
},
layers: [
{
id: "raster-layer",
type: "raster",
source: "raster-tiles"
}
]
};
}
const center = Array.isArray(coordinates)
? [coordinates[1], coordinates[0]] as [number, number]
: [coordinates.lng, coordinates.lat] as [number, number];
const mapInstance = new MapLibreGLMap({
container: containerRef.current,
style,
center,
zoom,
minZoom: 1,
renderWorldCopies: false
});
mapRef.current = mapInstance;
// Add navigation buttons.
mapInstance.addControl(new NavigationControl({
showCompass: false,
showZoom: true
}), "top-left");
setMap(mapInstance);
// Load async vector style if needed.
if (layerData.type === "vector" && typeof layerData.style !== "string") {
layerData.style().then(asyncStyle => {
mapInstance.setStyle(asyncStyle as StyleSpecification);
});
}
return () => {
mapInstance.off();
mapInstance.remove();
setMap(null);
};
}, []);
// Load the layer asynchronously.
const [ layer, setLayer ] = useState<Layer>();
// React to layer changes.
useEffect(() => {
async function load() {
const layerData = MAP_LAYERS[layerName];
if (!map) return;
if (layerData.type === "vector") {
const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style());
await import("@maplibre/maplibre-gl-leaflet");
setLayer(L.maplibreGL({
style: style as any
}));
if (layerData.type === "vector") {
if (typeof layerData.style === "string") {
map.setStyle(layerData.style);
} else {
setLayer(L.tileLayer(layerData.url, {
attribution: layerData.attribution,
detectRetina: true,
noWrap: true
}));
layerData.style().then(asyncStyle => {
map.setStyle(asyncStyle as StyleSpecification);
});
}
} else {
map.setStyle({
version: 8,
sources: {
"raster-tiles": {
type: "raster",
tiles: [layerData.url],
tileSize: 256,
attribution: layerData.attribution
}
},
layers: [
{
id: "raster-layer",
type: "raster",
source: "raster-tiles"
}
]
});
}
load();
}, [ layerName ]);
// Attach layer to the map.
useEffect(() => {
const map = mapRef.current;
const layerToAdd = layer;
if (!map || !layerToAdd) return;
layerToAdd.addTo(map);
return () => layerToAdd.removeFrom(map);
}, [ mapRef, layer ]);
}, [ map, layerData ]);
// React to coordinate changes.
useEffect(() => {
if (!mapRef.current) return;
mapRef.current.setView(coordinates, zoom);
}, [ mapRef, coordinates, zoom ]);
if (!map) return;
const center = Array.isArray(coordinates)
? [coordinates[1], coordinates[0]] as [number, number]
: [coordinates.lng, coordinates.lat] as [number, number];
map.setCenter(center);
map.setZoom(zoom);
}, [ map, coordinates, zoom ]);
// Viewport callback.
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const updateFn = () => viewportChanged(map.getBounds().getCenter(), map.getZoom());
const updateFn = () => {
const center = map.getCenter();
viewportChanged({ lat: center.lat, lng: center.lng }, map.getZoom());
};
map.on("moveend", updateFn);
map.on("zoomend", updateFn);
return () => {
map.off("moveend", updateFn);
map.off("zoomend", updateFn);
};
}, [ mapRef, viewportChanged ]);
}, [ map, viewportChanged ]);
useEffect(() => {
if (onClick && mapRef.current) {
mapRef.current.on("click", onClick);
return () => mapRef.current?.off("click", onClick);
}
}, [ mapRef, onClick ]);
if (!onClick || !map) return;
const handler = (e: MapMouseEvent) => onClick(toMapLibreEvent(e));
map.on("click", handler);
return () => { map.off("click", handler); };
}, [ map, onClick ]);
useEffect(() => {
if (onContextMenu && mapRef.current) {
mapRef.current.on("contextmenu", onContextMenu);
return () => mapRef.current?.off("contextmenu", onContextMenu);
}
}, [ mapRef, onContextMenu ]);
if (!onZoom || !map) return;
useEffect(() => {
if (onZoom && mapRef.current) {
mapRef.current.on("zoom", onZoom);
return () => mapRef.current?.off("zoom", onZoom);
}
}, [ mapRef, onZoom ]);
map.on("zoom", onZoom);
return () => { map.off("zoom", onZoom); };
}, [ map, onZoom ]);
// Scale
useEffect(() => {
const map = mapRef.current;
if (!scale || !map) return;
const scaleControl = control.scale();
scaleControl.addTo(map);
return () => scaleControl.remove();
}, [ mapRef, scale ]);
const scaleControl = new ScaleControl();
map.addControl(scaleControl);
return () => { map.removeControl(scaleControl); };
}, [ map, scale ]);
// Adapt to container size changes.
const size = useElementSize(containerRef);
useEffect(() => {
mapRef.current?.invalidateSize();
}, [ size?.width, size?.height ]);
map?.resize();
}, [ map, size?.width, size?.height ]);
return (
<div
ref={containerRef}
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
>
<ParentMap.Provider value={mapRef.current}>
<ParentMap.Provider value={map}>
{children}
</ParentMap.Provider>
</div>

View File

@@ -1,20 +1,23 @@
export interface MapLayer {
name: string;
isDarkTheme?: boolean;
}
import { type StyleSpecification } from "maplibre-gl";
interface VectorLayer extends MapLayer {
export type MapLayer = ({
type: "vector";
style: string | (() => Promise<{}>)
}
interface RasterLayer extends MapLayer {
style: string | (() => Promise<StyleSpecification>);
styleFallback: StyleSpecification;
} | {
type: "raster";
url: string;
attribution: string;
}
}) & {
// Common properties
name: string;
isDarkTheme?: boolean;
};
export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
// Minimal empty style used as a placeholder while the real style loads asynchronously.
const EMPTY_STYLE: StyleSpecification = { version: 8, sources: {}, layers: [] };
export const MAP_LAYERS: Record<string, MapLayer> = {
"openstreetmap": {
name: "OpenStreetMap",
type: "raster",
@@ -24,28 +27,33 @@ export const MAP_LAYERS: Record<string, VectorLayer | RasterLayer> = {
"versatiles-colorful": {
name: "VersaTiles Colorful",
type: "vector",
style: async () => (await import("./styles/colorful/en.json")).default
style: async () => (await import("./styles/colorful/en.json")).default as unknown as StyleSpecification,
styleFallback: EMPTY_STYLE
},
"versatiles-eclipse": {
name: "VersaTiles Eclipse",
type: "vector",
style: async () => (await import("./styles/eclipse/en.json")).default,
style: async () => (await import("./styles/eclipse/en.json")).default as unknown as StyleSpecification,
styleFallback: EMPTY_STYLE,
isDarkTheme: true
},
"versatiles-graybeard": {
name: "VersaTiles Graybeard",
type: "vector",
style: async () => (await import("./styles/graybeard/en.json")).default
style: async () => (await import("./styles/graybeard/en.json")).default as unknown as StyleSpecification,
styleFallback: EMPTY_STYLE,
},
"versatiles-neutrino": {
name: "VersaTiles Neutrino",
type: "vector",
style: async () => (await import("./styles/neutrino/en.json")).default
style: async () => (await import("./styles/neutrino/en.json")).default as unknown as StyleSpecification,
styleFallback: EMPTY_STYLE,
},
"versatiles-shadow": {
name: "VersaTiles Shadow",
type: "vector",
style: async () => (await import("./styles/shadow/en.json")).default,
style: async () => (await import("./styles/shadow/en.json")).default as unknown as StyleSpecification,
styleFallback: EMPTY_STYLE,
isDarkTheme: true
}
};

View File

@@ -1,71 +1,208 @@
import { useContext, useEffect } from "preact/hooks";
import { ParentMap } from "./map";
import { DivIcon, GPX, GPXOptions, Icon, LatLng, Marker as LeafletMarker, LeafletMouseEvent, marker, MarkerOptions } from "leaflet";
import "leaflet-gpx";
import { Marker as MapLibreMarker } from "maplibre-gl";
import { useContext, useEffect, useRef } from "preact/hooks";
import { GeoMouseEvent,ParentMap } from "./map";
export interface MarkerProps {
coordinates: [ number, number ];
icon?: Icon | DivIcon;
iconHtml?: string;
iconSize?: [number, number];
iconAnchor?: [number, number];
onClick?: () => void;
onMouseDown?: (e: MouseEvent) => void;
onDragged?: ((newCoordinates: LatLng) => void);
onContextMenu: (e: LeafletMouseEvent) => void;
onDragged?: ((newCoordinates: { lat: number; lng: number }) => void);
onContextMenu: (e: GeoMouseEvent) => void;
draggable?: boolean;
}
export default function Marker({ coordinates, icon, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
export default function Marker({ coordinates, iconHtml, iconSize, iconAnchor, draggable, onClick, onDragged, onMouseDown, onContextMenu }: MarkerProps) {
const parentMap = useContext(ParentMap);
const markerRef = useRef<MapLibreMarker>(null);
useEffect(() => {
if (!parentMap) return;
const options: MarkerOptions = { icon };
if (draggable) {
options.draggable = true;
options.autoPan = true;
options.autoPanSpeed = 5;
const el = document.createElement("div");
el.className = "geo-marker";
if (iconHtml) {
el.innerHTML = iconHtml;
}
if (iconSize) {
el.style.width = `${iconSize[0]}px`;
el.style.height = `${iconSize[1]}px`;
}
const newMarker = marker(coordinates, options);
const newMarker = new MapLibreMarker({
element: el,
draggable: !!draggable,
anchor: "bottom"
})
.setLngLat([coordinates[1], coordinates[0]])
.addTo(parentMap);
markerRef.current = newMarker;
if (onClick) {
newMarker.on("click", () => onClick());
el.addEventListener("click", (e) => {
e.stopPropagation();
onClick();
});
}
if (onMouseDown) {
newMarker.on("mousedown", e => onMouseDown(e.originalEvent));
el.addEventListener("mousedown", (e) => {
if (e.button === 1) {
e.stopPropagation();
onMouseDown(e);
}
});
}
if (onDragged) {
newMarker.on("moveend", e => {
const coordinates = (e.target as LeafletMarker).getLatLng();
onDragged(coordinates);
newMarker.on("dragend", () => {
const lngLat = newMarker.getLngLat();
onDragged({ lat: lngLat.lat, lng: lngLat.lng });
});
}
if (onContextMenu) {
newMarker.on("contextmenu", e => onContextMenu(e))
el.addEventListener("contextmenu", (e) => {
e.stopPropagation();
e.preventDefault();
const lngLat = newMarker.getLngLat();
onContextMenu({
latlng: { lat: lngLat.lat, lng: lngLat.lng },
originalEvent: e
});
});
}
newMarker.addTo(parentMap);
return () => {
newMarker.remove();
markerRef.current = null;
};
}, [ parentMap, coordinates, onMouseDown, onDragged, iconHtml ]);
return () => newMarker.removeFrom(parentMap);
}, [ parentMap, coordinates, onMouseDown, onDragged, icon ]);
return (<div />)
return (<div />);
}
export function GpxTrack({ gpxXmlString, options }: { gpxXmlString: string, options: GPXOptions }) {
export interface GpxTrackProps {
gpxXmlString: string;
trackColor?: string;
startIconHtml?: string;
endIconHtml?: string;
waypointIconHtml?: string;
}
export function GpxTrack({ gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml }: GpxTrackProps) {
const parentMap = useContext(ParentMap);
useEffect(() => {
if (!parentMap) return;
const track = new GPX(gpxXmlString, options);
track.addTo(parentMap);
const markers: MapLibreMarker[] = [];
const sourceId = `gpx-source-${Math.random().toString(36).slice(2)}`;
const layerId = `gpx-layer-${sourceId}`;
return () => track.removeFrom(parentMap);
}, [ parentMap, gpxXmlString, options ]);
function addGpxToMap() {
const parser = new DOMParser();
const gpxDoc = parser.parseFromString(gpxXmlString, "application/xml");
// Parse tracks.
const coordinates: [number, number][] = [];
const trackPoints = gpxDoc.querySelectorAll("trkpt, rtept");
for (const pt of trackPoints) {
const lat = parseFloat(pt.getAttribute("lat") ?? "0");
const lon = parseFloat(pt.getAttribute("lon") ?? "0");
coordinates.push([lon, lat]);
}
// Add GeoJSON line for the track.
if (coordinates.length > 0) {
parentMap.addSource(sourceId, {
type: "geojson",
data: {
type: "Feature",
properties: {},
geometry: {
type: "LineString",
coordinates
}
}
});
parentMap.addLayer({
id: layerId,
type: "line",
source: sourceId,
paint: {
"line-color": trackColor ?? "blue",
"line-width": 3
}
});
// Start marker
if (startIconHtml) {
const startEl = document.createElement("div");
startEl.className = "geo-marker";
startEl.innerHTML = startIconHtml;
const startMarker = new MapLibreMarker({ element: startEl, anchor: "bottom" })
.setLngLat(coordinates[0])
.addTo(parentMap);
markers.push(startMarker);
}
// End marker
if (endIconHtml && coordinates.length > 1) {
const endEl = document.createElement("div");
endEl.className = "geo-marker";
endEl.innerHTML = endIconHtml;
const endMarker = new MapLibreMarker({ element: endEl, anchor: "bottom" })
.setLngLat(coordinates[coordinates.length - 1])
.addTo(parentMap);
markers.push(endMarker);
}
}
// Parse waypoints.
const waypoints = gpxDoc.querySelectorAll("wpt");
for (const wpt of waypoints) {
const lat = parseFloat(wpt.getAttribute("lat") ?? "0");
const lon = parseFloat(wpt.getAttribute("lon") ?? "0");
if (waypointIconHtml) {
const wptEl = document.createElement("div");
wptEl.className = "geo-marker";
wptEl.innerHTML = waypointIconHtml;
const wptMarker = new MapLibreMarker({ element: wptEl, anchor: "bottom" })
.setLngLat([lon, lat])
.addTo(parentMap);
markers.push(wptMarker);
}
}
}
if (parentMap.isStyleLoaded()) {
addGpxToMap();
} else {
parentMap.once("style.load", addGpxToMap);
}
return () => {
for (const m of markers) {
m.remove();
}
try {
if (parentMap.getLayer(layerId)) {
parentMap.removeLayer(layerId);
}
if (parentMap.getSource(sourceId)) {
parentMap.removeSource(sourceId);
}
} catch {
// Map may be already removed.
}
};
}, [ parentMap, gpxXmlString, trackColor, startIconHtml, endIconHtml, waypointIconHtml ]);
return <div />;
}

View File

@@ -1,5 +1,5 @@
.note-list {
overflow: hidden;
overflow: visible;
position: relative;
height: 100%;
}
@@ -128,6 +128,10 @@
border: 1px solid transparent;
}
body.mobile .note-list.grid-view .note-book-card {
flex-basis: 150px;
}
.note-list.grid-view .note-book-card {
max-height: 300px;
}

View File

@@ -1,11 +1,12 @@
import { EventData } from "../../components/app_context.js";
import { LOCALES } from "@triliumnext/commons";
import { readCssVar } from "../../utils/css-var.js";
import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import { EventData } from "../../components/app_context.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
import options from "../../services/options.js";
import utils, { isMobile } from "../../services/utils.js";
import { readCssVar } from "../../utils/css-var.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -17,14 +18,12 @@ import { getEnabledExperimentalFeatureIds } from "../../services/experimental_fe
* - `#root-container.vertical-layout`, if the current layout is horizontal.
*/
export default class RootContainer extends FlexContainer<BasicWidget> {
private originalViewportHeight: number;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.id("root-widget");
this.css("height", "100dvh");
this.originalViewportHeight = getViewportHeight();
}
render(): JQuery<HTMLElement> {
@@ -39,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
this.#initPWATopbarColor();
return super.render();
}
@@ -64,8 +64,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
#onMobileResize() {
const currentViewportHeight = getViewportHeight();
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const windowHeight = window.innerHeight;
// If viewport is significantly smaller, keyboard is likely open
const isKeyboardOpened = windowHeight - viewportHeight > 150;
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
@@ -88,7 +92,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
#setBackdropEffects() {
const enabled = options.is("backdropEffectsEnabled");
const enabled = options.is("backdropEffectsEnabled") && !isMobile();
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
@@ -96,7 +100,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
// Supports background effects
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
.asBoolean(false);
.asBoolean(false);
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
@@ -112,8 +116,23 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
document.body.lang = locale;
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}
#initPWATopbarColor() {
if (!utils.isPWA()) return;
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}
}
function getViewportHeight() {
return window.visualViewport?.height ?? window.innerHeight;
}

View File

@@ -1,14 +1,26 @@
.scrolling-container {
--content-margin-inline: 24px;
overflow: auto;
scroll-behavior: smooth;
position: relative;
> .inline-title,
> .note-detail > .note-detail-editable-text,
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor,
> .note-list-widget:not(.full-height) .note-list-wrapper {
padding-inline: 24px;
margin-inline: var(--content-margin-inline);
}
> .inline-title {
padding-inline: var(--content-margin-inline);
}
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor {
overflow: unset;
}
}
body.mobile .scrolling-container {
--content-margin-inline: 8px;
}
.note-split.type-code:not(.mime-text-x-sqlite) {

View File

@@ -32,6 +32,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
display: flex;
align-items: center;
margin-block: 0;
.note-icon-widget {
display: flex;
align-items: center;
margin-inline-start: 0;
}
}
.modal.popup-editor-dialog .modal-header .note-title-widget {
@@ -91,8 +97,9 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
height: 100%;
}
.modal.popup-editor-dialog .note-detail-editable-text {
padding: 0 1em;
.modal.popup-editor-dialog .note-detail-editable-text-editor {
margin: 0 28px;
overflow: visible; /* Allow selection rectangle to go outside of the editor area */
}
.modal.popup-editor-dialog .note-detail-file {

View File

@@ -5,13 +5,15 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import tree from "../../services/tree";
import utils from "../../services/utils";
import NoteList from "../collections/NoteList";
import FloatingButtons from "../FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import { DESKTOP_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import NoteBadges from "../layout/NoteBadges";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import NoteDetail from "../NoteDetail";
@@ -23,8 +25,6 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
import NoteBadges from "../layout/NoteBadges";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
@@ -34,7 +34,7 @@ export default function PopupEditor() {
const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor"));
const isMobile = utils.isMobile();
const items = useMemo(() => {
const baseItems = isMobile ? MOBILE_FLOATING_BUTTONS : DESKTOP_FLOATING_BUTTONS;
const baseItems = isMobile ? [] : DESKTOP_FLOATING_BUTTONS;
return baseItems.filter(item => !POPUP_HIDDEN_FLOATING_BUTTONS.includes(item));
}, [ isMobile ]);
@@ -67,10 +67,7 @@ export default function PopupEditor() {
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<>
<TitleRow />
{isNewLayout && <NoteBadges />}
</>}
title={<TitleRow />}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: t("popup-editor.maximize"),
@@ -123,6 +120,7 @@ export function TitleRow() {
<div className="title-row">
<NoteIcon />
<NoteTitleWidget />
{isNewLayout && <NoteBadges />}
</div>
);
}

View File

@@ -1,11 +1,16 @@
import { useContext, useMemo } from "preact/hooks";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
import "./BookmarkButtons.css";
import { CSSProperties } from "preact";
import { useContext, useMemo } from "preact/hooks";
import type FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks";
import NoteLink from "../react/NoteLink";
import { CustomNoteLauncher } from "./GenericButtons";
import ResponsiveContainer from "../react/ResponsiveContainer";
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
const PARENT_NOTE_ID = "_lbBookmarks";
@@ -19,17 +24,64 @@ export default function BookmarkButtons() {
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
)
<ResponsiveContainer
desktop={
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
}
mobile={
<LaunchBarDropdownButton
icon="bx bx-bookmark"
title={t("bookmark_buttons.bookmarks")}
>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</LaunchBarDropdownButton>
}
/>
);
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
return <ResponsiveContainer
desktop={
bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
mobile={<MobileBookmarkItem noteId={note.noteId} bookmarkFolder={bookmarkFolder} />}
/>;
}
function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) {
const note = useNote(noteId);
const noteIcon = useNoteIcon(note);
if (!note) return null;
return (
!bookmarkFolder
? <FormListItem icon={noteIcon} onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title}</FormListItem>
: <MobileBookmarkFolder note={note} />
);
}
function MobileBookmarkFolder({ note }: { note: FNote }) {
const childNotes = useChildNotes(note.noteId);
return (
<FormDropdownSubmenu icon="bx bx-folder" title={note.title}>
{childNotes.map(childNote => (
<FormListItem
key={childNote.noteId}
icon={childNote.getIcon()}
onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })}
>
{childNote.title}
</FormListItem>
))}
</FormDropdownSubmenu>
);
}
function BookmarkFolder({ note }: { note: FNote }) {
@@ -55,5 +107,5 @@ function BookmarkFolder({ note }: { note: FNote }) {
</ul>
</div>
</LaunchBarDropdownButton>
)
);
}

View File

@@ -7,32 +7,18 @@ import { isCtrlKey } from "../../services/utils";
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
export function CustomNoteLauncher(props: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
keyboardShortcut?: string;
}) {
const { launcherNote, getTargetNoteId } = props;
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
if (evt.which === 3) {
return;
}
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
await launchCustomNoteLauncher(evt, props);
}, [ props ]);
// Keyboard shortcut.
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
@@ -54,3 +40,24 @@ export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNo
/>
);
}
export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
}) {
if (evt.which === 3) return;
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}

View File

@@ -49,6 +49,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
placement: isHorizontalLayout ? "bottom" : "right"
}
}}
mobileBackdrop
{...props}
>{children}</Dropdown>
);

View File

@@ -0,0 +1,282 @@
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { openInAppHelpFromUrl } from "../../services/utils";
import { BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useNoteContext, useTriliumEvent } from "../react/hooks";
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
interface ActiveContentInfo {
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
isEnabled: boolean;
canToggleEnabled: boolean;
}
const executeOption: BookProperty = {
type: "button",
icon: "bx bx-play",
label: t("active_content_badges.menu_execute_now"),
onClick: context => context.triggerCommand("runActiveNote")
};
const typeMappings: Record<ActiveContentInfo["type"], {
title: string;
icon: string;
helpPage: string;
apiDocsPage?: string;
isExecutable?: boolean;
additionalOptions?: BookProperty[];
}> = {
iconPack: {
title: t("active_content_badges.type_icon_pack"),
icon: "bx bx-package",
helpPage: "g1mlRoU8CsqC",
},
backendScript: {
title: t("active_content_badges.type_backend_script"),
icon: "bx bx-server",
helpPage: "SPirpZypehBG",
apiDocsPage: "MEtfsqa5VwNi",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") },
{ value: "daily", label: t("active_content_badges.menu_run_daily") },
{ value: "hourly", label: t("active_content_badges.menu_run_hourly") }
]
}
]
},
frontendScript: {
title: t("active_content_badges.type_frontend_script"),
icon: "bx bx-window",
helpPage: "yIhgI5H7A2Sm",
apiDocsPage: "Q2z6av6JZVWm",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") },
{ value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") },
]
},
{ type: "separator" },
{
type: "button",
label: t("active_content_badges.menu_change_to_widget"),
icon: "bx bxs-widget",
onClick: ({ note }) => attributes.setLabel(note.noteId, "widget")
}
]
},
widget: {
title: t("active_content_badges.type_widget"),
icon: "bx bxs-widget",
helpPage: "MgibgPcfeuGz",
additionalOptions: [
{
type: "button",
label: t("active_content_badges.menu_change_to_frontend_script"),
icon: "bx bx-window",
onClick: ({ note }) => {
attributes.removeOwnedLabelByName(note, "widget");
attributes.removeOwnedLabelByName(note, "disabled:widget");
}
}
]
},
appCss: {
title: t("active_content_badges.type_app_css"),
icon: "bx bxs-file-css",
helpPage: "AlhDUqhENtH7"
},
renderNote: {
title: t("active_content_badges.type_render_note"),
icon: "bx bx-extension",
helpPage: "HcABDtFCkbFN"
},
webView: {
title: t("active_content_badges.type_web_view"),
icon: "bx bx-globe",
helpPage: "1vHRoWCEjj0L"
},
appTheme: {
title :t("active_content_badges.type_app_theme"),
icon: "bx bx-palette",
helpPage: "7NfNr5pZpVKV",
additionalOptions: [
{
type: "combobox",
bindToLabel: "appThemeBase",
label: t("active_content_badges.menu_theme_base"),
icon: "bx bx-layer",
dropStart: true,
options: [
{ label: t("theme.auto_theme"), value: null },
{ type: "separator" },
{ label: t("theme.triliumnext"), value: "next" },
{ label: t("theme.triliumnext-light"), value: "next-light" },
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
]
}
]
}
};
export function ActiveContentBadges() {
const { note } = useNoteContext();
const info = useActiveContentInfo(note);
return (note && info &&
<>
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
<ActiveContentBadge info={info} note={note} />
</>
);
}
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type];
return (
<BadgeWithDropdown
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
icon={icon}
text={title}
dropdownOptions={{
dropdownContainerClassName: "mobile-bottom-menu",
mobileBackdrop: true
}}
>
{additionalOptions?.length && (
<>
{additionalOptions?.map((property, i) => (
<ViewProperty key={i} note={note} property={property} />
))}
<FormDropdownDivider />
</>
)}
<FormListItem
icon="bx bx-help-circle"
onClick={() => openInAppHelpFromUrl(helpPage)}
>{t("active_content_badges.menu_docs")}</FormListItem>
{apiDocsPage && <FormListItem
icon="bx bx-book-content"
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
</BadgeWithDropdown>
);
}
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
const { title } = typeMappings[info.type];
return info && <FormToggle
switchOnName="" switchOffName=""
currentValue={info.isEnabled}
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
onChange={async (willEnable) => {
await Promise.all(note.getOwnedAttributes()
.map(attr => ({ name: attributes.getNameWithoutDangerousPrefix(attr.name), type: attr.type }))
.filter(({ name, type }) => DANGEROUS_ATTRIBUTES.some(item => item.name === name && item.type === type))
.map(({ name, type }) => attributes.toggleDangerousAttribute(note, type, name, willEnable)));
}}
/>;
}
function useActiveContentInfo(note: FNote | null | undefined) {
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
function refresh() {
let type: ActiveContentInfo["type"] | null = null;
let isEnabled = false;
let canToggleEnabled = false;
if (!note) {
setInfo(null);
return;
}
if (note.type === "render") {
type = "renderNote";
isEnabled = note.hasRelation("renderNote");
} else if (note.type === "webView") {
type = "webView";
isEnabled = note.hasLabel("webViewSrc");
} else if (note.type === "code" && note.mime === "application/javascript;env=backend") {
type = "backendScript";
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
isEnabled ||= note.hasLabel(backendLabel);
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
canToggleEnabled = true;
}
}
} else if (note.type === "code" && note.mime === "application/javascript;env=frontend") {
type = "frontendScript";
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
} else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) {
isEnabled = note.hasLabel("appTheme");
canToggleEnabled = true;
}
for (const labelToCheck of activeContentLabels) {
if (note.hasLabel(labelToCheck)) {
type = labelToCheck;
isEnabled = true;
canToggleEnabled = true;
break;
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
type = labelToCheck;
isEnabled = false;
canToggleEnabled = true;
break;
}
}
if (type) {
setInfo({ type, isEnabled, canToggleEnabled });
} else {
setInfo(null);
}
}
// Refresh on note change.
useEffect(refresh, [ note ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
refresh();
}
});
return info;
}

View File

@@ -37,6 +37,10 @@
pointer-events: none;
}
}
&.active-content-badge { --color: var(--badge-active-content-background-color); }
&.active-content-badge.disabled {
opacity: 0.5;
}
min-width: 0;
@@ -45,6 +49,11 @@
text-overflow: ellipsis;
min-width: 0;
}
.switch-button {
--switch-track-height: 8px;
--switch-track-width: 30px;
}
}
.dropdown-badge {

View File

@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
export default function NoteBadges() {
return (
@@ -19,6 +20,7 @@ export default function NoteBadges() {
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
<ActiveContentBadges />
</div>
);
}

View File

@@ -50,6 +50,9 @@ body.experimental-feature-new-layout {
}
}
}
body.desktop .title-actions {
> .collapsible,
> .note-type-switcher {
padding-inline-start: calc(24px - var(--title-actions-padding-start));
@@ -57,3 +60,4 @@ body.experimental-feature-new-layout {
}
}
}

View File

@@ -1,25 +1,21 @@
import "./NoteTitleActions.css";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import CollectionProperties from "../note_bars/CollectionProperties";
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
import SimpleBadge from "../react/Badge";
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
import NoteLink, { NewNoteLink } from "../react/NoteLink";
import { NewNoteLink } from "../react/NoteLink";
import { useEditedNotes } from "../ribbon/EditedNotesTab";
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
import NoteTypeSwitcher from "./NoteTypeSwitcher";
export default function NoteTitleActions() {
const { note, ntxId, componentId, noteContext } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const { note, ntxId, componentId, noteContext, viewScope } = useNoteContext();
const noteType = useNoteProperty(note, "type");
return (
@@ -27,7 +23,7 @@ export default function NoteTitleActions() {
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
<EditedNotes />
<NoteTypeSwitcher />
{(!viewScope?.viewMode || viewScope.viewMode === "default") && <NoteTypeSwitcher />}
</div>
);
}
@@ -48,7 +44,7 @@ function PromotedAttributes({ note, componentId, noteContext }: {
componentId: string,
noteContext: NoteContext | undefined
}) {
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
const [ expanded, setExpanded ] = useState(false);
useEffect(() => {

View File

@@ -37,6 +37,10 @@
&:hover {
background: var(--input-background-color);
}
.text {
white-space: nowrap;
}
}
.status-bar-dropdown-button {
@@ -58,101 +62,12 @@
.dropdown-note-info {
padding: 1em !important;
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.dropdown-note-paths {
.note-paths-widget {
padding: 0.5em;
}
.note-path-intro {
color: var(--muted-text-color);
}
.note-path-list {
margin: 12px 0;
padding: 0;
list-style: none;
/* Note path card */
li {
--border-radius: 6px;
position: relative;
background: var(--card-background-color);
padding: 8px 20px 8px 25px;
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
& + li {
margin-top: 2px;
}
/* Current path arrow */
&.path-current::before {
position: absolute;
display: flex;
justify-content: flex-end;
align-items: center;
content: "\ee8f";
top: 0;
left: 0;
width: 20px;
bottom: 0;
font-family: "boxicons";
font-size: .75em;
color: var(--menu-item-icon-color);
}
}
/* Note path segment */
a {
margin-inline: 2px;
padding-inline: 2px;
color: currentColor;
font-weight: normal;
text-decoration: none;
/* The last segment of the note path */
&.basename {
color: var(--muted-text-color);
}
}
}
}
.backlinks-widget > .dropdown-menu {
@@ -160,84 +75,6 @@
max-height: 60vh;
overflow-y: scroll;
/* Backlink card */
li {
--border-radius: 8px;
max-width: 600px;
padding: 10px 20px;
background: var(--card-background-color);
& + li {
margin-top: 2px;
}
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
> span {
display: flex;
flex-wrap: wrap;
align-items: center;
/* Note path */
> small {
flex: 100%;
order: -1;
font-size: .65rem;
.note-path {
padding: 0;
}
}
/* Note icon */
> .tn-icon {
color: var(--menu-item-icon-color);
}
/* Note title */
> a {
margin-inline-start: 4px;
color: currentColor;
font-weight: 500;
}
}
}
/* Card content - excerpt */
& > span:nth-child(2) > div {
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
display: block;
margin: 8px 0;
border-radius: 4px;
background: var(--quick-search-result-content-background);
padding: 8px;
font-size: .75rem;
a {
background: transparent;
color: var(--quick-search-result-highlight-color);
text-decoration: underline;
}
p {
margin: 0;
}
}
}
}
}
@@ -284,16 +121,50 @@
}
div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
button.select-button:not(:focus-visible) {
outline: none;
}
}
body.experimental-feature-new-layout .note-info-content {
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
padding: 0;
}
.bottom-panel {
margin: 0 !important;
padding: 0;

View File

@@ -56,7 +56,7 @@ export default function StatusBar() {
similarNotesShown: activePane === "similar-notes",
setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes")
};
const isHiddenNote = note?.isInHiddenSubtree();
const isHiddenNote = note?.isHiddenCompletely();
return (
<div className="status-bar">
@@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) {
//#region Note info & Similar
interface NoteInfoContext extends StatusBarContext {
similarNotesShown: boolean;
setSimilarNotesShown: (value: boolean) => void;
similarNotesShown?: boolean;
setSimilarNotesShown?: (value: boolean) => void;
}
export function NoteInfoBadge(context: NoteInfoContext) {
@@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) {
// Keyboard shortcut.
useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show());
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown));
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown));
return (enabled &&
<StatusBarDropdown
@@ -242,8 +242,8 @@ export function NoteInfoBadge(context: NoteInfoContext) {
);
}
function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: NoteInfoContext & {
dropdownRef: RefObject<BootstrapDropdown>;
export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick<NoteInfoContext, "note" | "setSimilarNotesShown"> & {
dropdownRef?: RefObject<BootstrapDropdown>;
noteType: NoteType;
}) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
@@ -251,7 +251,7 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]);
return (
<>
<div className="note-info-content">
<ul>
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
@@ -262,14 +262,14 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul>
<LinkButton
{setSimilarNotesShown && <LinkButton
text={t("note_info_widget.show_similar_notes")}
onClick={() => {
dropdownRef.current?.hide();
dropdownRef?.current?.hide();
setSimilarNotesShown(true);
}}
/>
</>
/>}
</div>
);
}
@@ -300,7 +300,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
return (note && count > 0 &&
<StatusBarDropdown
className="backlinks-badge backlinks-widget"
className="backlinks-badge backlinks-widget tn-backlinks-widget"
icon="bx bx-link"
text={t("status_bar.backlinks", { count })}
title={t("status_bar.backlinks_title", { count })}

View File

@@ -35,11 +35,6 @@
display: flex;
flex-direction: column;
&.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
&.active {
outline: 4px solid var(--more-accented-background-color);
background: var(--card-background-hover-color);
@@ -89,12 +84,25 @@
&.type-text {
padding: 10px;
--ck-content-todo-list-checkmark-size: 8px;
p { margin-bottom: 0.2em;}
hr { margin-block: 0.1em; height: 1px; }
h2 { font-size: 1.20em; }
h3 { font-size: 1.15em; }
h4 { font-size: 1.10em; }
h5 { font-size: 1.05em}
h6 { font-size: 1em; }
ul, ol { margin: 0 }
}
&.type-book,
&.type-contentWidget,
&.type-search,
&.type-empty {
&.type-empty,
&.type-relationMap,
&.type-launcher,
&.tab-preview-placeholder {
display: flex;
align-items: center;
justify-content: center;
@@ -105,13 +113,6 @@
.preview-placeholder {
font-size: 500%;
}
p { margin-bottom: 0.2em;}
h2 { font-size: 1.20em; }
h3 { font-size: 1.15em; }
h4 { font-size: 1.10em; }
h5 { font-size: 1.05em}
h6 { font-size: 1em; }
}
&.with-split {

View File

@@ -1,6 +1,7 @@
import "./TabSwitcher.css";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { createPortal, Fragment } from "preact/compat";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -11,6 +12,7 @@ import contextMenu from "../../menus/context_menu";
import { getHue, parseColor } from "../../services/css_class_manager";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import type { ViewMode, ViewScope } from "../../services/link";
import { NoteContent } from "../collections/legacy/ListOrGridView";
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
@@ -20,6 +22,13 @@ import Icon from "../react/Icon";
import LinkButton from "../react/LinkButton";
import Modal from "../react/Modal";
const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
source: "bx bx-code",
"contextual-help": "bx bx-help-circle",
"note-map": "bx bxs-network-chart",
attachments: "bx bx-paperclip",
};
export default function TabSwitcher() {
const [ shown, setShown ] = useState(false);
const mainNoteContexts = useMainNoteContexts();
@@ -138,7 +147,6 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
activeNtxId: string | null | undefined;
}) {
const { note } = noteContext;
const iconClass = useNoteIcon(note);
const colorClass = note?.getColorClass() || '';
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
const subContexts = noteContext.getSubContexts();
@@ -158,46 +166,65 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
>
{subContexts.map(subContext => (
<Fragment key={subContext.ntxId}>
<header className={colorClass}>
{subContext.note && <Icon icon={iconClass} />}
<span className="title">{subContext.note?.title ?? t("tab_row.new_tab")}</span>
{subContext.isMainContext() && <ActionButton
icon="bx bx-x"
text={t("tab_row.close_tab")}
onClick={(e) => {
// We are closing a tab, so we need to prevent propagation for click (activate tab).
e.stopPropagation();
appContext.tabManager.removeNoteContext(subContext.ntxId);
}}
/>}
</header>
<div className={clsx("tab-preview", `type-${subContext.note?.type ?? "empty"}`)}>
<TabPreviewContent note={subContext.note} />
</div>
<TabHeader noteContext={subContext} colorClass={colorClass} />
<TabPreviewContent note={subContext.note} viewScope={subContext.viewScope} />
</Fragment>
))}
</div>
);
}
function TabPreviewContent({ note }: {
note: FNote | null
}) {
if (!note) {
return <PreviewPlaceholder icon="bx bx-plus" />;
}
function TabHeader({ noteContext, colorClass }: { noteContext: NoteContext, colorClass: string }) {
const iconClass = useNoteIcon(noteContext.note);
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
if (note.type === "book") {
return <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
}
// Manage the title for read-only notes
useEffect(() => {
noteContext?.getNavigationTitle().then(setNavigationTitle);
}, [noteContext]);
return (
<NoteContent
<header className={colorClass}>
{noteContext.note && <Icon icon={iconClass} />}
<span className="title">{navigationTitle ?? noteContext.note?.title ?? t("tab_row.new_tab")}</span>
{noteContext.isMainContext() && <ActionButton
icon="bx bx-x"
text={t("tab_row.close_tab")}
onClick={(e) => {
// We are closing a tab, so we need to prevent propagation for click (activate tab).
e.stopPropagation();
appContext.tabManager.removeNoteContext(noteContext.ntxId);
}}
/>}
</header>
);
}
function TabPreviewContent({ note, viewScope }: {
note: FNote | null,
viewScope: ViewScope | undefined
}) {
let el: ComponentChild;
let isPlaceholder = true;
if (!note) {
el = <PreviewPlaceholder icon="bx bx-plus" />;
} else if (note.type === "book") {
el = <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
} else if (viewScope?.viewMode && viewScope.viewMode !== "default") {
el = <PreviewPlaceholder icon={VIEW_MODE_ICON_MAPPINGS[viewScope?.viewMode ?? ""] ?? "bx bx-empty"} />;
} else {
el = <NoteContent
note={note}
highlightedTokens={undefined}
trim
includeArchivedNotes={false}
/>
/>;
isPlaceholder = false;
}
return (
<div className={clsx("tab-preview", `type-${note?.type ?? "empty"}`, { "tab-preview-placeholder": isPlaceholder })}>{el}</div>
);
}

View File

@@ -0,0 +1,3 @@
.code-note-switcher-modal .dropdown-menu {
background: none !important;
}

View File

@@ -1,84 +1,240 @@
import { useContext } from "preact/hooks";
import "./mobile_detail_menu.css";
import appContext, { CommandMappings } from "../../components/app_context";
import contextMenu, { MenuItem } from "../../menus/context_menu";
import branches from "../../services/branches";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import { createPortal, useRef, useState } from "preact/compat";
import FNote, { NotePathRecord } from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import note_create from "../../services/note_create";
import tree from "../../services/tree";
import { openInAppHelpFromUrl } from "../../services/utils";
import BasicWidget from "../basic_widget";
import server from "../../services/server";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import { getLocaleName, NoteInfoContent } from "../layout/StatusBar";
import ActionButton from "../react/ActionButton";
import { ParentComponent } from "../react/react_utils";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import Modal from "../react/Modal";
import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import { NoteContextMenu } from "../ribbon/NoteActions";
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
export default function MobileDetailMenu() {
const parentComponent = useContext(ParentComponent);
const dropdownRef = useRef<BootstrapDropdown | null>(null);
const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext();
const subContexts = noteContext?.getMainContext().getSubContexts() ?? [];
const isMainContext = noteContext?.isMainContext();
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false);
const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false);
const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false);
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
function closePane() {
// Wait first for the context menu to be dismissed, otherwise the backdrop stays on.
requestAnimationFrame(() => {
parentComponent.triggerCommand("closeThisNoteSplit", { ntxId });
});
}
return (
<ActionButton
icon="bx bx-dots-vertical-rounded"
text=""
onClick={(e) => {
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
if (!ntxId) return;
<div style={{ contain: "none" }}>
{note ? (
<NoteContextMenu
dropdownRef={dropdownRef}
note={note} noteContext={noteContext}
itemsAtStart={<>
<div className="form-list-row">
<div className="form-list-col">
<FormListItem
icon="bx bx-link"
onClick={() => setBacklinksModalShown(true)}
disabled={backlinksCount === 0}
>{t("status_bar.backlinks", { count: backlinksCount })}</FormListItem>
</div>
<div className="form-list-col">
<FormListItem
icon="bx bx-directions"
onClick={() => setNotePathsModalShown(true)}
disabled={(sortedNotePaths?.length ?? 0) <= 1}
>{t("status_bar.note_paths", { count: sortedNotePaths?.length })}</FormListItem>
</div>
</div>
<FormDropdownDivider />
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
const subContexts = noteContext.getMainContext().getSubContexts();
const isMainContext = noteContext?.isMainContext();
const note = noteContext.note;
const helpUrl = getHelpUrlForNote(note);
{noteContext && ntxId && <NoteActionsCustom note={note} noteContext={noteContext} ntxId={ntxId} />}
<FormListItem
onClick={() => noteContext?.notePath && note_create.createNote(noteContext.notePath)}
icon="bx bx-plus"
>{t("mobile_detail_menu.insert_child_note")}</FormListItem>
{subContexts.length < 2 && <>
<FormDropdownDivider />
<FormListItem
onClick={(e) => {
// We have to manually manage the hide because otherwise the old note context gets activated.
e.stopPropagation();
dropdownRef.current?.hide();
parentComponent.triggerCommand("openNewNoteSplit", { ntxId });
}}
icon="bx bx-dock-right"
>{t("create_pane_button.create_new_split")}</FormListItem>
</>}
{!isMainContext && <>
<FormDropdownDivider />
<FormListItem
icon="bx bx-x"
onClick={closePane}
>{t("close_pane_button.close_this_pane")}</FormListItem>
</>}
<FormDropdownDivider />
</>}
itemsNearNoteSettings={<>
{note.type === "text" && <ContentLanguageSelector note={note} />}
{note.type === "code" && <FormListItem icon={"bx bx-code"} onClick={() => setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}</FormListItem>}
<FormListItem icon="bx bx-info-circle" onClick={() => setNoteInfoModalShown(true)}>{t("note_info_widget.title")}</FormListItem>
<FormListItem icon="bx bx-bar-chart" onClick={() => setSimilarNotesModalShown(true)}>{t("similar_notes.title")}</FormListItem>
<FormDropdownDivider />
</>}
/>
) : (
<ActionButton
icon="bx bx-x"
onClick={closePane}
text={t("close_pane_button.close_this_pane")}
/>
)}
const items: (MenuItem<keyof CommandMappings>)[] = [
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
{ kind: "separator" },
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
{ kind: "separator" },
helpUrl && {
title: t("help-button.title"),
uiIcon: "bx bx-help-circle",
handler: () => openInAppHelpFromUrl(helpUrl)
},
{ kind: "separator" },
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
const lastItem = items.at(-1);
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
items.pop();
}
contextMenu.show<keyof CommandMappings>({
x: e.pageX,
y: e.pageY,
items,
selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") {
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
} else if (command === "delete") {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (!notePath) {
throw new Error("Cannot get note path to delete.");
}
const branchId = await tree.getBranchIdFromUrl(notePath);
if (!branchId) {
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
}
if (await branches.deleteNotes([branchId]) && parentComponent) {
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
}
} else if (command && parentComponent) {
parentComponent.triggerCommand(command, { ntxId });
}
},
forcePositionOnMobile: true
});
}}
/>
{createPortal((
<>
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
<NoteInfoModal note={note} modalShown={noteInfoModalShown} setModalShown={setNoteInfoModalShown} />
<SimilarNotesModal note={note} modalShown={similarNotesModalShown} setModalShown={setSimilarNotesModalShown} />
<CodeNoteSwitcherModal note={note} modalShown={codeNoteSwitcherModalShown} setModalShown={setCodeNoteSwitcherModalShown} />
</>
), document.body)}
</div>
);
}
function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) {
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
return (
<FormDropdownSubmenu
icon="bx bx-globe"
title={t("mobile_detail_menu.content_language_switcher", { language: getLocaleName(activeLocale ?? DEFAULT_LOCALE) })}
>
{processedLocales.map((locale, index) =>
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentNoteLanguage}
onClick={() => setCurrentNoteLanguage(locale.id)}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
)}
</FormDropdownSubmenu>
);
}
interface WithModal {
modalShown: boolean;
setModalShown: (shown: boolean) => void;
}
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="backlinks-modal tn-backlinks-widget"
size="md"
title={t("mobile_detail_menu.backlinks")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<ul className="backlinks-items">
{note && <BacklinksList note={note} />}
</ul>
</Modal>
);
}
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) {
return (
<Modal
className="note-paths-modal"
size="md"
title={t("note_paths.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
{note && (
<NotePathsWidget
sortedNotePaths={sortedNotePaths}
currentNotePath={notePath}
/>
)}
</Modal>
);
}
function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="note-info-modal"
size="md"
title={t("note_info_widget.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
{note && <NoteInfoContent note={note} noteType={note.type} />}
</Modal>
);
}
function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="similar-notes-modal"
size="md"
title={t("similar_notes.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<SimilarNotesTab note={note} />
</Modal>
);
}
function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
const currentNoteMime = useNoteProperty(note, "mime");
const mimeTypes = useMimeTypes();
return (
<Modal
className="code-note-switcher-modal"
size="md"
title={t("status_bar.code_note_switcher")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<div className="dropdown-menu static show">
{note && <NoteTypeCodeNoteList
currentMimeType={currentNoteMime}
mimeTypes={mimeTypes}
changeNoteType={(type, mime) => {
server.put(`notes/${note.noteId}/type`, { type, mime });
setModalShown(false);
}}
/>}
</div>
</Modal>
);
}

View File

@@ -5,7 +5,7 @@
align-items: center;
width: 100%;
max-width: unset;
font-size: 0.8em;
font-size: 0.8rem;
.dropdown-menu {
input.form-control {

View File

@@ -2,18 +2,16 @@ import "./CollectionProperties.css";
import { t } from "i18next";
import { ComponentChildren } from "preact";
import { useContext, useRef } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import { useRef } from "preact/hooks";
import FNote from "../../entities/fnote";
import { ViewTypeOptions } from "../collections/interface";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteProperty, useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu";
import { bookPropertiesConfig } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
@@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
<Dropdown
buttonClassName="bx bx-cog icon-action"
hideToggleArrow
dropdownContainerClassName="mobile-bottom-menu"
mobileBackdrop
>
{properties.map(property => (
<ViewProperty key={property.label} note={note} property={property} />
{properties.map((property, index) => (
<ViewProperty key={index} note={note} property={property} />
))}
{properties.length > 0 && <FormDropdownDivider />}
@@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@@ -117,3 +117,35 @@ body.experimental-feature-new-layout {
}
}
}
body.mobile .modal.icon-switcher {
.modal-dialog {
left: 0;
right: 0;
margin: unset;
transform: unset;
max-width: 100%;
height: 100%;
}
.modal-body {
padding: 0;
display: flex;
flex-direction: column;
> .filter-row {
padding: 0.25em var(--bs-modal-padding) 0.5em var(--bs-modal-padding);
border-bottom: 1px solid var(--main-border-color);
}
}
.icon-list {
margin: auto;
flex-grow: 1;
height: 100%;
span {
padding: 12px;
}
}
}

View File

@@ -4,7 +4,8 @@ import { IconRegistry } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { t } from "i18next";
import { CSSProperties, RefObject } from "preact";
import { CSSProperties } from "preact";
import { createPortal } from "preact/compat";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type React from "react";
import { CellComponentProps, Grid } from "react-window";
@@ -12,11 +13,15 @@ import { CellComponentProps, Grid } from "react-window";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import server from "../services/server";
import { isDesktop, isMobile } from "../services/utils";
import ActionButton from "./react/ActionButton";
import Dropdown from "./react/Dropdown";
import { FormDropdownDivider, FormListItem } from "./react/FormList";
import FormTextBox from "./react/FormTextBox";
import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks";
import { useNoteContext, useNoteLabel, useStaticTooltip, useWindowSize } from "./react/hooks";
import Modal from "./react/Modal";
const ICON_SIZE = isMobile() ? 56 : 48;
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
@@ -37,6 +42,10 @@ export default function NoteIcon() {
setIcon(note?.getIcon());
}, [ note, iconClass, workspaceIconClass ]);
if (isMobile()) {
return <MobileNoteIconSwitcher note={note} icon={icon} />;
}
return (
<Dropdown
className="note-icon-widget"
@@ -48,16 +57,47 @@ export default function NoteIcon() {
hideToggleArrow
disabled={viewScope?.viewMode !== "default"}
>
{ note && <NoteIconList note={note} dropdownRef={dropdownRef} /> }
{ note && <NoteIconList note={note} onHide={() => dropdownRef?.current?.hide()} columnCount={12} /> }
</Dropdown>
);
}
function NoteIconList({ note, dropdownRef }: {
note: FNote,
dropdownRef: RefObject<BootstrapDropdown>;
function MobileNoteIconSwitcher({ note, icon }: {
note: FNote | null | undefined;
icon: string | null | undefined;
}) {
const [ modalShown, setModalShown ] = useState(false);
const { windowWidth } = useWindowSize();
return (note &&
<div className="note-icon-widget">
<ActionButton
className="note-icon"
icon={icon ?? "bx bx-empty"}
text={t("note_icon.change_note_icon")}
onClick={() => setModalShown(true)}
/>
{createPortal((
<Modal
title={t("note_icon.change_note_icon")}
size="xl"
show={modalShown} onHidden={() => setModalShown(false)}
className="icon-switcher note-icon-widget"
scrollable
>
<NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />
</Modal>
), document.body)}
</div>
);
}
function NoteIconList({ note, onHide, columnCount }: {
note: FNote;
onHide: () => void;
columnCount: number;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const iconListRef = useRef<HTMLDivElement>(null);
const [ search, setSearch ] = useState<string>();
const [ filterByPrefix, setFilterByPrefix ] = useState<string | null>(null);
@@ -73,53 +113,22 @@ function NoteIconList({ note, dropdownRef }: {
return (
<>
<div class="filter-row">
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
placeholder={ filterByPrefix
? t("note_icon.search_placeholder_filtered", {
number: filteredIcons.length ?? 0,
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
})
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
currentValue={search} onChange={setSearch}
autoFocus
/>
{getIconLabels(note).length > 0 && (
<div style={{ textAlign: "center" }}>
<ActionButton
icon="bx bx-reset"
text={t("note_icon.reset-default")}
onClick={() => {
if (!note) return;
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
dropdownRef?.current?.hide();
}}
/>
</div>
)}
{glob.iconRegistry.sources.length > 0 && <Dropdown
buttonClassName="bx bx-filter-alt"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
title={t("note_icon.filter")}
>
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>}
</div>
<FilterRow
note={note}
filterByPrefix={filterByPrefix}
search={search}
setSearch={setSearch}
setFilterByPrefix={setFilterByPrefix}
filteredIcons={filteredIcons}
onHide={onHide}
/>
<div
class="icon-list"
ref={iconListRef}
style={{
width: (columnCount * ICON_SIZE + 10),
}}
onClick={(e) => {
// Make sure we are not clicking on something else than a button.
const clickedTarget = e.target as HTMLElement;
@@ -130,18 +139,19 @@ function NoteIconList({ note, dropdownRef }: {
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
attributes.setLabel(note.noteId, attributeToSet, iconClass);
}
dropdownRef?.current?.hide();
onHide();
}}
>
{filteredIcons.length ? (
<Grid
columnCount={12}
columnWidth={48}
rowCount={Math.ceil(filteredIcons.length / 12)}
rowHeight={48}
columnCount={columnCount}
columnWidth={ICON_SIZE}
rowCount={Math.ceil(filteredIcons.length / columnCount)}
rowHeight={ICON_SIZE}
cellComponent={IconItemCell}
cellProps={{
filteredIcons
filteredIcons,
columnCount
}}
/>
) : (
@@ -152,10 +162,95 @@ function NoteIconList({ note, dropdownRef }: {
);
}
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
function FilterRow({ note, filterByPrefix, search, setSearch, setFilterByPrefix, filteredIcons, onHide }: {
note: FNote;
filterByPrefix: string | null;
search: string | undefined;
setSearch: (value: string | undefined) => void;
setFilterByPrefix: (value: string | null) => void;
filteredIcons: IconWithName[];
onHide: () => void;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const hasCustomIcon = getIconLabels(note).length > 0;
function resetToDefaultIcon() {
if (!note) return;
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
onHide();
}
return (
<div class="filter-row">
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
placeholder={ filterByPrefix
? t("note_icon.search_placeholder_filtered", {
number: filteredIcons.length ?? 0,
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
})
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
currentValue={search} onChange={setSearch}
autoFocus
/>
{isDesktop()
? <>
{hasCustomIcon && (
<div style={{ textAlign: "center" }}>
<ActionButton
icon="bx bx-reset"
text={t("note_icon.reset-default")}
onClick={resetToDefaultIcon}
/>
</div>
)}
{<Dropdown
buttonClassName="bx bx-filter-alt"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
title={t("note_icon.filter")}
>
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>}
</> : (
<Dropdown
buttonClassName="bx bx-dots-vertical-rounded"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
dropdownContainerClassName="mobile-bottom-menu"
>
{hasCustomIcon && <>
<FormListItem
icon="bx bx-reset"
onClick={resetToDefaultIcon}
disabled={!hasCustomIcon}
>{t("note_icon.reset-default")}</FormListItem>
<FormDropdownDivider />
</>}
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>
)}
</div>
);
}
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons, columnCount }: CellComponentProps<{
filteredIcons: IconWithName[];
columnCount: number;
}>) {
const iconIndex = rowIndex * 12 + columnIndex;
const iconIndex = rowIndex * columnCount + columnIndex;
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
if (!iconData) return <></> as React.ReactElement;
@@ -184,7 +279,7 @@ function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
checked={filterByPrefix === "bx"}
onClick={() => setFilterByPrefix("bx")}
>{t("note_icon.filter-default")}</FormListItem>
<FormDropdownDivider />
{glob.iconRegistry.sources.length > 1 && <FormDropdownDivider />}
{glob.iconRegistry.sources.map(({ prefix, name, icon }) => (
prefix !== "bx" && <FormListItem

View File

@@ -109,4 +109,29 @@ body.experimental-feature-new-layout {
--input-focus-color: initial;
}
}
&.mobile .title-row {
.icon-action:not(.note-icon) {
--icon-button-size: 45px;
--icon-button-icon-ratio: 0.5;
flex-shrink: 0;
}
.note-actions {
width: auto;
}
.note-badges {
margin-inline: 0.5em;
flex-shrink: 0;
}
.note-icon-widget {
margin-inline: 0.5em;
.note-icon {
--icon-button-size: 24px;
}
}
}
}

View File

@@ -1,4 +1,4 @@
#left-pane .tree-wrapper {
.tree-wrapper {
.note-indicator-icon.subtree-hidden-badge {
font-family: inherit !important;
margin-inline: 0.5em;
@@ -9,7 +9,6 @@
border-radius: 0.5em;
font-size: 0.7rem;
font-weight: normal;
float: right;
vertical-align: middle;
}

View File

@@ -1992,7 +1992,7 @@ function buildEnhanceTitle() {
if (isSubtreeHidden && count > 0) {
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
$span.find(".fancytree-title").append($badge);
$span.append($badge);
}
};
}

View File

@@ -43,7 +43,8 @@ export const TYPE_MAPPINGS: Record<ExtendedNoteType, NoteTypeMapping> = {
},
protectedSession: {
view: () => import("./type_widgets/ProtectedSession"),
className: "protected-session-password-component"
className: "protected-session-password-component",
isFullHeight: true
},
book: {
view: () => import("./type_widgets/Book"),

View File

@@ -3,10 +3,7 @@
line-height: 1em;
display: flex;
align-items: center;
appearance: none;
background: transparent;
border: 0;
color: inherit;
padding-inline-end: 12px;
.arrow {
font-size: 1.3em;
@@ -17,21 +14,34 @@
.collapsible-body {
height: 0;
overflow: hidden;
&.fully-expanded {
overflow: visible;
}
}
.collapsible-inner-body {
padding-top: 0.5em;
opacity: 0;
}
&.expanded {
.collapsible-title .arrow {
transform: rotate(90deg);
}
.collapsible-inner-body {
opacity: 1;
}
}
&.with-transition {
.collapsible-body {
transition: height 250ms ease-in;
}
.collapsible-inner-body {
transition: opacity 250ms ease-in;
}
}
}

View File

@@ -27,6 +27,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
const { height } = useElementSize(innerRef) ?? {};
const contentId = useUniqueName();
const [ transitionEnabled, setTransitionEnabled ] = useState(false);
const [ fullyExpanded, setFullyExpanded ] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
@@ -35,13 +36,28 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
return () => clearTimeout(timeout);
}, []);
useEffect(() => {
if (expanded) {
if (transitionEnabled) {
const timeout = setTimeout(() => {
setFullyExpanded(true);
}, 250);
return () => clearTimeout(timeout);
} else {
setFullyExpanded(true);
}
} else {
setFullyExpanded(false);
}
}, [expanded, transitionEnabled])
return (
<div className={clsx("collapsible", className, {
expanded,
"with-transition": transitionEnabled
})}>
<button
className="collapsible-title"
className="collapsible-title tn-low-profile"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
aria-controls={contentId}
@@ -53,7 +69,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
<div
id={contentId}
ref={bodyRef}
className="collapsible-body"
className={clsx("collapsible-body", {"fully-expanded": fullyExpanded})}
style={{ height: expanded ? height : "0" }}
aria-hidden={!expanded}
>

View File

@@ -3,6 +3,7 @@ import { ComponentChildren, HTMLAttributes } from "preact";
import { CSSProperties, HTMLProps } from "preact/compat";
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import { isMobile } from "../../services/utils";
import { useTooltip, useUniqueName } from "./hooks";
type DataAttributes = {
@@ -32,9 +33,10 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
dropdownRef?: MutableRef<BootstrapDropdown | null>;
titlePosition?: "top" | "right" | "bottom" | "left";
titleOptions?: Partial<Tooltip.Options>;
mobileBackdrop?: boolean;
}
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
@@ -74,12 +76,18 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
setShown(true);
externalOnShown?.();
hideTooltip();
}, [ hideTooltip ]);
if (mobileBackdrop && isMobile()) {
document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover");
}
}, [ hideTooltip, mobileBackdrop ]);
const onHidden = useCallback(() => {
setShown(false);
externalOnHidden?.();
}, []);
if (mobileBackdrop && isMobile()) {
document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover");
}
}, [ mobileBackdrop ]);
useEffect(() => {
if (!containerRef.current) return;

View File

@@ -3,8 +3,9 @@ import { useEffect, useRef } from "preact/hooks";
import ActionButton, { ActionButtonProps } from "./ActionButton";
import Button, { ButtonProps } from "./Button";
import { FormListItem, FormListItemOpts } from "./FormList";
interface FormFileUploadProps {
export interface FormFileUploadProps {
name?: string;
onChange: (files: FileList | null) => void;
multiple?: boolean;
@@ -75,3 +76,25 @@ export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit<Ac
</>
);
}
/**
* Similar to {@link FormFileUploadButton}, but uses an {@link FormListItem} instead of a normal {@link Button}.
* @param param the change listener for the file upload and the properties for the button.
*/
export function FormFileUploadFormListItem({ onChange, children, ...buttonProps }: Omit<FormListItemOpts, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FormListItem
{...buttonProps}
onClick={() => inputRef.current?.click()}
>{children}</FormListItem>
<FormFileUpload
inputRef={inputRef}
hidden
onChange={onChange}
/>
</>
);
}

View File

@@ -27,3 +27,13 @@
}
}
}
.form-list-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
.form-list-col {
flex-grow: 1;
}
}

View File

@@ -78,7 +78,7 @@ export interface FormListBadge {
text: string;
}
interface FormListItemOpts {
export interface FormListItemOpts {
children: ComponentChildren;
icon?: string;
value?: string;

View File

@@ -0,0 +1,212 @@
import { FilterLabelsByType } from "@triliumnext/commons";
import { Fragment, VNode } from "preact";
import { useContext } from "preact/hooks";
import Component from "../../components/component";
import FNote from "../../entities/fnote";
import NoteContextAwareWidget from "../note_context_aware_widget";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList";
import FormTextBox from "./FormTextBox";
import { useNoteLabel, useNoteLabelBoolean } from "./hooks";
import { ParentComponent } from "./react_utils";
export interface ClickContext {
note: FNote;
triggerCommand: NoteContextAwareWidget["triggerCommand"];
}
export interface CheckBoxProperty {
type: "checkbox",
label: string;
bindToLabel: FilterLabelsByType<boolean>;
icon?: string;
/** When true, the checkbox will be checked when the label value is false. Useful when the label represents a "hide" action, without exposing double negatives to the user. */
reverseValue?: boolean;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: ClickContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
width?: number;
min?: number;
icon?: string;
disabled?: (note: FNote) => boolean;
}
export interface ComboBoxItem {
/**
* The value to set to the bound label, `null` has a special meaning which removes the label entirely.
*/
value: string | null;
label: string;
}
export interface ComboBoxGroup {
title: string;
items: ComboBoxItem[];
}
interface Separator {
type: "separator"
}
export interface ComboBoxProperty {
type: "combobox",
label: string;
icon?: string;
bindToLabel: FilterLabelsByType<string>;
/**
* The default value is used when the label is not set.
*/
defaultValue?: string;
options: (ComboBoxItem | Separator | ComboBoxGroup)[];
dropStart?: boolean;
}
export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator;
export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
case "separator":
return <FormDropdownDivider />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const ItemsComponent = property.items;
const clickContext = parentComponent && {
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
};
return (parentComponent &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
//@ts-expect-error Interop with text box which takes in string values even for numbers.
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const disabled = property.disabled?.(note);
return (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabel(note, property.bindToLabel);
const valueWithDefault = value ?? property.defaultValue ?? null;
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={valueWithDefault === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
dropStart={property.dropStart}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
if ("type" in option) {
return <FormDropdownDivider key={index} />;
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={ property.reverseValue ? !value : value }
onChange={newValue => setValue(property.reverseValue ? !newValue : newValue)}
/>
);
}

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