Compare commits

...

396 Commits

Author SHA1 Message Date
Elian Doran
bb36b33694 Bump to 0.90.12 2024-11-24 11:35:06 +02:00
Elian Doran
e982696ef4 Merge pull request #663 from hasecilu/i18n/Spanish
I18n/spanish
2024-11-24 11:32:45 +02:00
Elian Doran
c294469f12 Merge remote-tracking branch 'origin/develop' 2024-11-23 14:04:36 +02:00
Elian Doran
2d8fb4eff5 chore(i18n): fix punctuation 2024-11-23 00:48:01 +02:00
Elian Doran
79b31bda76 chore(i18n): reach 100% for Romanian + small change 2024-11-22 19:48:57 +02:00
Elian Doran
c30a4373d9 Merge pull request #652 from TriliumNext/remove-renovate-action
Remove the renovate action in favor of the GH app
2024-11-22 19:43:54 +02:00
perf3ct
12065902d2 Remove the renovate action in favor of the GH app 2024-11-22 17:39:34 +00:00
Elian Doran
aa01161a40 Merge pull request #651 from TriliumNext/tab_enhance
Add reopen_last_tab and copy_tab_to_new_window to  tab management
2024-11-22 19:01:20 +02:00
SiriusXT
3cfc2ac768 Add reopen_last_tab and copy_tab_to_new_window to tab management 2024-11-22 17:24:06 +08:00
Elian Doran
79a906e695 Merge pull request #649 from TriliumNext/perfectra1n-patch-2
Also run Docker healthcheck checks on PRs
2024-11-21 23:11:22 +02:00
Elian Doran
2ffd0de736 feat(client): translate Electron context menu 2024-11-21 20:58:54 +02:00
Elian Doran
ea8e98b8ef refactor(client): define context menu shortcuts in separate field 2024-11-21 20:33:47 +02:00
Jon Fuller
84b555de3c Also run Docker healthcheck checks on PRs 2024-11-21 09:39:13 -08:00
Elian Doran
a037f95ff1 Merge pull request #625 from TriliumNext/renovate/migrate-config
chore(config): migrate renovate config
2024-11-21 19:07:00 +02:00
renovate[bot]
50d9f382a1 chore(config): migrate config renovate.json 2024-11-21 16:53:03 +00:00
Elian Doran
ed90e0f7a9 client: Change tree star icon to link (closes #565) 2024-11-21 18:37:37 +02:00
Elian Doran
8c7cba4f33 server: Add a new settings launcher (closes #619) 2024-11-21 18:11:08 +02:00
Adorian Doran
128c4d45df Add a separator to the editor's context menu running under Electron 2024-11-21 17:21:57 +02:00
Adorian Doran
8658f9e6d3 Prevent the global menu's zoom container to be highlighted when being hovered 2024-11-21 17:11:32 +02:00
Adorian Doran
312c3ed6ad Add an extra separator for the zoom controls in the global menu 2024-11-21 16:27:48 +02:00
Adorian Doran
bd2bcb7c97 Fix useless separator in the global menu when running under Electron 2024-11-21 16:21:24 +02:00
Elian Doran
ae85bffd08 Merge pull request #618 from TriliumNext/feat/tweak-menus
Tweak menus
2024-11-20 19:12:43 +02:00
Elian Doran
84f63d5cf7 client: Remove icon color for close button 2024-11-20 19:12:20 +02:00
Elian Doran
0f75319677 Merge remote-tracking branch 'origin/develop' into develop
; Conflicts:
;	src/public/translations/de/translation.json
2024-11-20 19:10:04 +02:00
Adorian Doran
050eb08b1a Close #613 2024-11-20 19:01:20 +02:00
Elian Doran
cdf8490651 Merge pull request #585 from TriliumNext/sirius_patch_2
Triggers full text search when Ctrl + Enter is pressed in note_autocomplete.
2024-11-20 18:54:20 +02:00
Adorian Doran
8f05b24694 Move back the close tab-related actions at the top of the menu 2024-11-20 14:39:19 +02:00
j13055
95f80efaeb fixed some errors 2024-11-20 13:24:14 +01:00
Adorian Doran
281b81ee60 Remove the shadow and the opening delay for the "Main Menu -> Advanced" submenu in mobile view 2024-11-20 14:18:17 +02:00
Adorian Doran
85b507938b Allow the submenu opening delay be set via a CSS variable 2024-11-20 14:16:10 +02:00
j13055
0c02a3bae9 tranlate not translated lines 2024-11-20 11:40:48 +01:00
Adorian Doran
ac9f344130 Retrigger the opening animation when repositioning menus that are already open 2024-11-20 11:30:45 +02:00
Adorian Doran
2b432dd4f7 Delay the opening of submenus 2024-11-20 10:57:34 +02:00
Adorian Doran
3d27a60897 Add a fade animation when a menu is opening 2024-11-20 10:48:42 +02:00
Adorian Doran
3792761ffc Add missing icons 2024-11-20 10:13:52 +02:00
Adorian Doran
322d261df7 Tweak the icons of the launcher context menu items 2024-11-20 09:48:20 +02:00
Adorian Doran
1fb58f3e87 Reorganize the launcher context menu 2024-11-20 09:38:20 +02:00
SiriusXT
c51adbc449 Add full text search in autocomplete 2024-11-20 14:22:39 +08:00
Adorian Doran
4179f9c155 Improve the sub-menu arrows for the tree context menu 2024-11-20 02:56:18 +02:00
Adorian Doran
35faba2c2f Fix the note revision list displaying a shadow 2024-11-20 02:30:29 +02:00
Adorian Doran
dc893a438e Refine the icons from the tree menu 2024-11-20 02:10:29 +02:00
Adorian Doran
a83e68fbb6 Change the "open externally" icons to avoid confusion with "open in a new tab" 2024-11-20 02:03:40 +02:00
Adorian Doran
a677f4381d Add icon for "Open note in a new tab" 2024-11-20 01:45:36 +02:00
Adorian Doran
98dfeee188 Update the icons of the tree context menu 2024-11-20 01:42:42 +02:00
Adorian Doran
dc7bb6d7eb Reorganize the tree context menu 2024-11-20 00:47:04 +02:00
Adorian Doran
855f936dbf Reorganize the attachment menu 2024-11-20 00:11:54 +02:00
Adorian Doran
48e7bab81b Reorganize the tab menu 2024-11-19 23:57:12 +02:00
Adorian Doran
694f896623 Highlight the "Delete note" menu item as a destructive action 2024-11-19 23:46:49 +02:00
Adorian Doran
5df287db23 Use a distinct icon color for destructive menu items 2024-11-19 23:44:57 +02:00
Elian Doran
8868a4eae1 Merge pull request #616 from TriliumNext/perfectra1n-patch-2
Update renovate.json
2024-11-19 23:40:38 +02:00
Elian Doran
0da1bee02c i18n: Fix typo 2024-11-19 23:39:43 +02:00
Elian Doran
779218849a i18n: Translate bulk action categories 2024-11-19 23:38:49 +02:00
Adorian Doran
4999809e3a Reorganize the note menu 2024-11-19 23:32:10 +02:00
Adorian Doran
fd5412b715 Merge branch 'develop' of https://github.com/TriliumNext/Notes into feat/tweak-menus 2024-11-19 23:16:13 +02:00
Adorian Doran
4bcca01ff3 Add a drop shadow for menus 2024-11-19 22:55:44 +02:00
Adorian Doran
9b5526c99f Tweak the color of the menu separator 2024-11-19 22:40:52 +02:00
Adorian Doran
80ce2f5dbd Reorder the global menu items 2024-11-19 22:31:29 +02:00
Jon Fuller
f629d48028 Update renovate.json
Remove the package rules for now, use the default schemas. Also include this repository in the config since I forgot that part 🤣
2024-11-19 12:08:17 -08:00
Elian Doran
da95e15b01 Merge pull request #615 from TriliumNext/feat/tweak-backup-list
Improve the "Existing backups" section
2024-11-19 20:40:30 +02:00
Adorian Doran
70be4cd1c2 Update the Romanian translation 2024-11-19 20:34:54 +02:00
Adorian Doran
349b1c1d78 Improve appeareance 2024-11-19 20:28:47 +02:00
Adorian Doran
e94942d665 Handle the situation where no backups are available 2024-11-19 20:22:10 +02:00
Elian Doran
4418ad986e Merge pull request #612 from meichthys/develop
Improve note revision wording and consistency
2024-11-19 20:22:07 +02:00
Elian Doran
c962a94e29 Merge pull request #607 from TriliumNext/add-renovate
Add renovate GitHub Action and JSON config
2024-11-19 20:21:05 +02:00
Elian Doran
7f3d5f1e70 Merge pull request #609 from TriliumNext/siriusxt_patch_1
Add box icons to note menu
2024-11-19 18:38:34 +02:00
Adorian Doran
32a4a9c072 Sort the backup files by date & time 2024-11-19 18:07:42 +02:00
Adorian Doran
22b768e5e8 Add translation 2024-11-19 18:00:23 +02:00
Adorian Doran
970c3bd7ad Format date and time 2024-11-19 17:54:34 +02:00
Adorian Doran
75941de449 Replace the "Existing backups" bulleted list with a table 2024-11-19 17:42:03 +02:00
MeIchthys
5d6a42b987 Improve note revision wording and consistency
Removed plurals where not needed, capitalized revisions dialog title, made note revision setting titles consistent with note revision dialog.
2024-11-19 14:22:52 +00:00
SiriusXT
d8e50a2ab8 add icons to the Attachments menu 2024-11-19 22:21:33 +08:00
Adorian Doran
5b050410cb Fix the action button tooltips for the "Existing tokens" table 2024-11-19 09:48:44 +02:00
SiriusXT
f7b1c3fee3 Add box icons to note menu 2024-11-19 14:03:30 +08:00
SiriusXT
0ba883ce2f Add box icons to note menu 2024-11-19 12:08:41 +08:00
SiriusXT
2b0d68368c Add box icons to note menu 2024-11-19 11:08:20 +08:00
perf3ct
180993ead9 make it clear that renovate opened this PR, for easier filtering 2024-11-18 20:22:38 +00:00
perf3ct
f4ed98ebda add renovate GitHub Action and json config 2024-11-18 20:16:39 +00:00
Elian Doran
a4c0ae06db client: Fix duplicate ribbon tabs (fixes #582) 2024-11-18 20:52:35 +02:00
Elian Doran
7a8d7f074c client: Fix share boxicon not working correctly (fixes #603) 2024-11-18 19:12:31 +02:00
Elian Doran
b4072ec8a5 Merge pull request #600 from dwong33/dwong33-patch-1
Created server.json, introduced Traditional Chinese translation
2024-11-17 18:47:58 +02:00
Dwong33
a50e3935b5 Rename zh/server.json to tw/server.json
Better suit the zh-tw vs zh-cn
2024-11-17 04:18:46 -05:00
Dwong33
3034ca217d Created server.json, introduced Traditional Chinese translation 2024-11-17 04:15:08 -05:00
SiriusXT
002839176e Triggers full text search when Ctrl + Enter is pressed 2024-11-17 12:18:05 +08:00
SiriusXT
e091ef64dd Triggers full text search when Ctrl + Enter is pressed 2024-11-17 12:17:11 +08:00
SiriusXT
46823d28e8 Merge branch 'develop' into sirius_patch_2 2024-11-17 12:15:29 +08:00
SiriusXT
616d7117db Merge branch 'sirius_patch_2' of https://github.com/TriliumNext/Notes into sirius_patch_2 2024-11-17 12:14:55 +08:00
SiriusXT
4e10071649 Triggers full text search when Ctrl + Enter is pressed in autocomplete 2024-11-17 12:14:44 +08:00
Elian Doran
3ff75b14e9 Merge pull request #595 from hasecilu/i18n/Spanish
i18n: Update Spanish translations for stable release
2024-11-16 00:25:08 +02:00
hasecilu
82e7814569 i18n: Update Spanish translations for stable release 2024-11-15 15:22:07 -06:00
Elian Doran
66e8cc40eb i18n: Translate Romanian strings 2024-11-15 22:34:09 +02:00
Elian Doran
2260dcefe5 client,server: Enforce min value of max content width (closes #593) 2024-11-15 22:29:59 +02:00
Elian Doran
40c9ef69e7 Merge pull request #587 from TriliumNext/feature/editor-type-radios
Replace the editor type combo box with radio buttons
2024-11-15 21:01:10 +02:00
Elian Doran
a8b87a1507 Merge pull request #576 from TriliumNext/siriusxt_patch
Add a text replacement feature to the find_widget
2024-11-15 20:51:27 +02:00
Elian Doran
1df1637257 Merge pull request #589 from TriliumNext/sirius_patch_3
Add more link protocol support
2024-11-15 20:43:09 +02:00
Adorian Doran
616cb87d4e client: Change the icon of the "Formatting" tab 2024-11-14 20:55:30 +02:00
SiriusXT
7f0d675ab8 Add more link protocol support 2024-11-14 14:04:10 +08:00
SiriusXT
c907b288bd Add more link protocol support 2024-11-14 11:18:03 +08:00
SiriusXT
d9ab5d71aa Add more link protocol support 2024-11-14 11:15:38 +08:00
Adorian Doran
8731b8a65b Fix translation 2024-11-13 23:55:30 +02:00
Adorian Doran
de4f06d9be Update translations 2024-11-13 23:35:10 +02:00
Adorian Doran
9485067749 client: Replace the editor type combo box with radio buttons 2024-11-13 23:34:43 +02:00
j13055
cd35706147 added missing translations 2024-11-13 13:59:36 +01:00
j13055
3b94aee7b7 finished server translations 2024-11-13 12:39:50 +01:00
j13055
06e30674fe corrected setup translations 2024-11-13 12:31:51 +01:00
j13055
f7b1e87bc4 corrected login translations 2024-11-13 12:26:30 +01:00
j13055
2c252a9984 finished keyboard_actions translations 2024-11-13 12:25:11 +01:00
SiriusXT
db79f231a0 Triggers full text search when Ctrl + Enter is pressed in note_autocomplete 2024-11-13 17:13:07 +08:00
Elian Doran
693bcfb587 client: Add find & replace button to fixed toolbar 2024-11-12 19:47:50 +02:00
Elian Doran
38d32813d2 client: Fix syntax highlight for shell scripts (closes #583) 2024-11-12 19:32:38 +02:00
SiriusXT
a0c6d695b0 Fix find_widget bugs 2024-11-12 10:56:54 +08:00
SiriusXT
d63baa1503 Merge branch 'develop' into siriusxt_patch 2024-11-12 08:59:53 +08:00
Elian Doran
6734d765c9 Bump to 0.90.11-beta 2024-11-11 19:48:50 +02:00
Elian Doran
470594b1c7 Merge pull request #573 from TriliumNext/perfectra1n-patch-2
Update README to mention MacOS command fix
2024-11-11 19:42:56 +02:00
Elian Doran
782d34566d Merge pull request #577 from hasecilu/i18n/Spanish_mini
i18n: Update Spanish translations, 100%
2024-11-11 19:41:32 +02:00
hasecilu
1b2a772612 i18n: Update Spanish translations, 100% 2024-11-11 10:35:54 -06:00
SiriusXT
497c24ee1e Fix the bug that code can't get the selected text 2024-11-11 23:13:26 +08:00
SiriusXT
8893e9d4d5 add replacement feature for code note 2024-11-11 22:57:24 +08:00
SiriusXT
2d9376a05c add a text replacement feature to the find_widget 2024-11-11 18:59:03 +08:00
SiriusXT
ce40c74e83 Merge branch 'develop' into siriusxt_patch 2024-11-11 18:26:07 +08:00
SiriusXT
0aef04cea1 add a text replacement feature to the find_widget 2024-11-11 18:19:19 +08:00
SiriusXT
12b71961ae add a text replacement feature to the find_widget 2024-11-11 18:11:31 +08:00
Elian Doran
46218d6ab4 Merge pull request #574 from TriliumNext/fix-version-update-check
More reliably check for version updates
2024-11-11 00:47:51 +02:00
perf3ct
1d2366fa06 fix "click to download" button 2024-11-10 17:18:26 +00:00
perf3ct
0acba0eac4 add docstring for func 2024-11-09 22:23:02 +00:00
perf3ct
48d53e276e more reliably check for version numbers 2024-11-09 22:16:00 +00:00
Elian Doran
47baa02bca i18n: Translate 100% of Romanian 2024-11-09 23:34:18 +02:00
Elian Doran
bc35c3c641 i18n: Remove some German-only messages 2024-11-09 23:31:45 +02:00
Jon Fuller
790b87f23f Update README to mention MacOS command fix 2024-11-09 13:28:45 -08:00
Elian Doran
48ba15ad88 i18n: Fix incorrect IDs for German 2024-11-09 23:25:34 +02:00
Elian Doran
cda28cfd65 Merge pull request #561 from j13055/develop
added german translation
2024-11-09 23:17:23 +02:00
Elian Doran
7ffe145481 Merge pull request #569 from TriliumNext/perfectra1n-patch-2
Update README.md for incremented sync version
2024-11-09 23:14:53 +02:00
Elian Doran
ac2bca790b Fix duplicate title for Trilium toolbar item (fixes #525) 2024-11-09 23:12:10 +02:00
Elian Doran
774966e640 client: Allow more link protocols (fixes #122) 2024-11-09 23:06:26 +02:00
Elian Doran
81310d33b0 Merge pull request #571 from TriliumNext/feature/classic_editor
Classic editor for text notes (with fixed toolbar)
2024-11-09 22:40:38 +02:00
Jon Fuller
34e6430977 Update README.md
Co-authored-by: Elian Doran <contact@eliandoran.me>
2024-11-09 12:30:52 -08:00
Elian Doran
15b4eacdca client: Change design of editor settings slightly 2024-11-09 21:35:37 +02:00
Elian Doran
7c342aed9e client: Use translations for editor settings 2024-11-09 21:34:09 +02:00
Elian Doran
8c69d47aed client,server: Implement shortcut for toggle classic editor toolbar 2024-11-09 18:36:38 +02:00
Elian Doran
f88d3220b5 client: Repair attribute editor 2024-11-09 18:09:05 +02:00
Elian Doran
70a98a3d33 client: Use refactored version of CKEditor 2024-11-09 15:40:14 +02:00
Elian Doran
6e0a10cf2c client: Hide ribbon tab when classic editor is off 2024-11-09 14:54:04 +02:00
Elian Doran
c421e75f55 client: Respect editor type choice 2024-11-09 14:49:05 +02:00
Elian Doran
89420eafa3 client: Set up ui for selecting editor UI 2024-11-09 14:33:20 +02:00
Elian Doran
7a70fc14b3 server: Set up editor type option 2024-11-09 14:33:14 +02:00
Elian Doran
d2008e7e5f client: Use different method to highlight disabled buttons 2024-11-09 14:15:03 +02:00
Elian Doran
745c9846a6 client: Use better method to expose CK watchdog 2024-11-09 14:13:08 +02:00
Elian Doran
3972bb2ecf client: Use build of CKEditor containing both types 2024-11-09 14:11:15 +02:00
Elian Doran
06262adf91 client: Use translation for classic toolbar title 2024-11-09 13:40:13 +02:00
Elian Doran
5771060b57 client: Reorganize classic toolbar 2024-11-09 13:39:24 +02:00
Elian Doran
6a11f9c073 client: Add some JSDoc 2024-11-09 10:46:12 +02:00
Elian Doran
85ee7def84 client: Improve loading feel for classic toolbar 2024-11-09 10:37:14 +02:00
Elian Doran
b88f0e0109 client: Hide ribbon for non text or read-only notes 2024-11-09 10:33:45 +02:00
Elian Doran
787aa6f5a6 client: Remove background for decoupled editor 2024-11-09 09:56:25 +02:00
Elian Doran
4f39188198 client: Use decoupled CKEditor 2024-11-09 09:43:37 +02:00
Elian Doran
dd6e762dab client: Activate ribbon toolbar by default 2024-11-09 09:19:38 +02:00
Elian Doran
48bc9204ac client: Create empty toolbar ribbon 2024-11-09 09:18:59 +02:00
Elian Doran
918f425e1f client: Group options for classic editor 2024-11-09 09:12:46 +02:00
Elian Doran
821af8dc11 client: Integrate block toolbar into classic options 2024-11-09 08:29:58 +02:00
Elian Doran
44734435ea client: Remove block toolbar in classic mode 2024-11-09 00:32:26 +02:00
Elian Doran
01c53b6d9f client: Use same config as bubble editor for classic 2024-11-09 00:21:27 +02:00
Elian Doran
9a5de0d4c8 client: Basic integration of classic editor w/ no attribute editor 2024-11-09 00:15:19 +02:00
Elian Doran
5116bddc5f client: Group image align buttons in CKEditor 2024-11-08 23:44:52 +02:00
Elian Doran
92aa671ec7 client: Support inline images in CKEditor (fixes #531) 2024-11-08 23:29:56 +02:00
Elian Doran
1f4d09f6f0 client: Patch CKEditor to fix IME (fixes #568)
See https://github.com/ckeditor/ckeditor5/pull/16289
2024-11-08 22:49:07 +02:00
Elian Doran
29e83b97e6 client: Fix rendering notes if hljs is not loaded 2024-11-08 21:50:22 +02:00
Jon Fuller
18de0857b3 Update README.md for incremented sync version 2024-11-08 10:43:45 -08:00
Elian Doran
2048a30aa5 Merge pull request #547 from TriliumNext/smaller-container
Make the container smaller
2024-11-08 19:20:48 +02:00
Elian Doran
78017e4d36 client: Improve classic toolbar layout on mobile 2024-11-08 00:26:20 +02:00
Elian Doran
35fe5845a3 client: Fix classic editor on mobile 2024-11-08 00:20:51 +02:00
Elian Doran
1261bdbb29 client: Use correct background for code note preview 2024-11-07 23:58:10 +02:00
Elian Doran
91fa1a6cb1 client: Add syntax highlight for code note previews 2024-11-07 23:53:02 +02:00
Elian Doran
1816fcd3ac client: force-graph: 1.45.0 -> 1.46.0 2024-11-07 23:11:22 +02:00
Elian Doran
d13044b972 client: mind-elixir: 4.3.0 -> 4.3.1 2024-11-07 23:09:59 +02:00
Elian Doran
f5205fdd30 electron: Fix code block theme loading in dev mode 2024-11-07 23:09:53 +02:00
Elian Doran
930b8e0ce2 Merge pull request #555 from rom1dep/mouse_scroll_dir
fix: mouse scroll wheel direction for zoom level
2024-11-07 22:25:24 +02:00
Elian Doran
b5988ba7c2 Merge pull request #559 from TriliumNext/siriusxt-test
Make attachments open in a new tab/browser
2024-11-07 22:22:42 +02:00
j13055
75e2ceed5d added german translation 2024-11-06 13:52:23 +01:00
SiriusXT
d2ee3738a2 Make attachments open in a new tab/browser 2024-11-06 10:02:42 +08:00
perf3ct
8a548f6589 also update the Alpine Dockerfile 2024-11-05 16:41:00 +00:00
perf3ct
0859a955b1 Results in a much smaller container 2024-11-04 17:38:05 -08:00
Elian Doran
a02146df17 server: Fix loading of code block theme on server builds 2024-11-05 02:58:21 +02:00
Elian Doran
a6385557b5 Merge pull request #545 from TriliumNext/latest-is-stable-container
Explicitly manage the "latest" tag, and have it point to the same tag as "stable"
2024-11-05 02:41:03 +02:00
Elian Doran
00aebfcdf0 Merge pull request #530 from Potjoe-97/patch-1
Patch fr translation
2024-11-05 02:38:53 +02:00
Elian Doran
c6b3ace807 client: mind-elixir: 4.2.4 -> 4.3.0 2024-11-05 02:33:12 +02:00
Elian Doran
6799544950 Update package-lock.json 2024-11-05 02:31:42 +02:00
Elian Doran
da1cf4d6ed Bump to 0.90.10-beta 2024-11-04 17:24:30 +02:00
Romain DEP.
21cfb64f83 fix: mouse scroll wheel direction 2024-11-03 23:01:01 +01:00
Adorian Doran
dd7c2084fa client: apply grouping to the MIME type list 2024-11-03 15:43:33 +02:00
Adorian Doran
4f5d874028 client: Use a multiple column layout for the MIME type listing 2024-11-03 15:42:13 +02:00
Potjoe-97
80e6276d31 Merge branch 'develop' into patch-1 2024-11-03 10:48:00 +01:00
Potjoe-97
0192060ad2 Update fr server.json : all strings translated 2024-11-03 10:44:56 +01:00
Potjoe-97
e41ff54c0d Update translation.json : all strings translated 2024-11-03 10:43:15 +01:00
perf3ct
bdece7216f have the latest tag be the same as stable tag
get rid of this annoying default "latest" tag useage

to squash

to squash, I love whitespace

don't need to verify
2024-11-02 21:51:06 +00:00
Elian Doran
611fb90a52 Merge pull request #544 from hasecilu/i18n/Spanish_update
i18n: Update Spanish translations
2024-11-02 21:45:41 +02:00
hasecilu
75e554d86b i18n: Update Spanish translations 2024-11-02 13:09:44 -06:00
Elian Doran
0db1a63cef client: Fix sync error toast 2024-11-02 19:02:26 +02:00
Elian Doran
4ffc6f716c client: Enable syntax highlighting in print 2024-11-02 16:40:33 +02:00
Elian Doran
fa3200ba8f electron: Fix docnotes not rendering 2024-11-02 16:11:59 +02:00
Elian Doran
bff9bedc44 i18n: Translate sync messages 2024-11-02 15:43:16 +02:00
Elian Doran
f8777b0de1 server: Fix path on dev environment 2024-11-02 15:01:58 +02:00
Adorian Doran
48e6c1a33d client: Properly align of the "Override theme fonts" checkbox 2024-11-02 14:34:55 +02:00
Adorian Doran
4c43ac5bdd client: Use a shadowless box for printed code blocks 2024-11-02 14:29:20 +02:00
Elian Doran
45ccc7562e client: Fix error in toast due to missing import 2024-11-02 12:19:17 +02:00
Elian Doran
e72eb5f27c electron: Fix asset path on forge build 2024-11-02 11:49:33 +02:00
Elian Doran
d1404492a7 build: Use shorter special version moniker
Some builds fail in the CI because the extra part of the version is limited to 20 chars.
2024-11-02 11:04:16 +02:00
Elian Doran
238c9c6f0d build: Fix updating nightly version for desktop builds 2024-11-02 10:43:42 +02:00
Elian Doran
443f02a78e client,server: i18next: 23.16.2 -> 23.16.4 2024-11-02 10:24:53 +02:00
Elian Doran
5fbd052138 build: Update tooling dependencies 2024-11-02 10:23:46 +02:00
Elian Doran
bc84a71929 client: mermaid: 11.3.0 -> 11.4.0 2024-11-02 10:22:22 +02:00
Elian Doran
a514a51fff client: mind-elixir: 4.2.3 -> 4.2.4 2024-11-02 10:18:58 +02:00
Elian Doran
24022834e2 db: Update demo section on code blocks 2024-11-02 10:15:22 +02:00
Elian Doran
9fdc84d91f build: Update nightly version for server as well 2024-11-02 09:53:36 +02:00
Elian Doran
9c27672794 build: Update nightly version to avoid caching issues 2024-11-02 09:47:31 +02:00
Elian Doran
f37fa3723b Merge pull request #526 from TriliumNext/feature/syntax_highlight
Basic syntax highlight support for code blocks
2024-11-02 01:46:02 +02:00
Elian Doran
b14065d442 server: Address self-review 2024-11-02 01:42:25 +02:00
Elian Doran
1554e25283 server: Add documentation for code_block_theme 2024-11-02 01:39:35 +02:00
Elian Doran
4e945583a1 server: Add some documentation 2024-11-02 00:55:45 +02:00
Elian Doran
92c588dc98 server: Implement color theme migration based on existing theme 2024-11-02 00:39:22 +02:00
Elian Doran
5c66e3fd04 server: Initialize code block theme for old databases as well 2024-11-02 00:20:27 +02:00
Elian Doran
e508313f21 electron: Fix deprecation warning 2024-11-01 23:42:32 +02:00
Elian Doran
df3f51d1f3 electron: Fix loading of highlight.js 2024-11-01 23:42:23 +02:00
Elian Doran
0a6815e448 Merge remote-tracking branch 'origin/develop' into feature/syntax_highlight 2024-11-01 23:20:12 +02:00
Elian Doran
293db6962e Merge branch 'develop' of ssh://github.com/TriliumNext/Notes into develop 2024-11-01 23:14:41 +02:00
Elian Doran
eb05c5b919 Merge pull request #534 from TriliumNext/AutomaticallyShowRecentNotes
Automatically trigger autocomplete on focus.
2024-11-01 20:37:39 +02:00
Elian Doran
bbaed45f6b client: Fix scrolling in empty tab search list after constraining height 2024-11-01 20:23:46 +02:00
Elian Doran
aa7d7b3afd client: Add borders to empty tab search list 2024-11-01 20:20:53 +02:00
Elian Doran
fc4797d04f Merge pull request #541 from TriliumNext/export_file_name
Crop fileName  and prevent cutting into the extension.
2024-11-01 19:44:31 +02:00
Elian Doran
c2baa4b752 server: Add comment to clarify use of regex 2024-11-01 19:43:39 +02:00
Elian Doran
faeefc75ba Merge pull request #542 from TriliumNext/close_tabs
close right tabs
2024-11-01 19:07:55 +02:00
Elian Doran
d0904c1051 client: Change translation for closing tabs to the right 2024-11-01 19:05:56 +02:00
Elian Doran
11a82e62f1 client: Change layout of tab context menu slightly 2024-11-01 19:03:06 +02:00
SiriusXT
7b24f7e332 close right tabs 2024-11-01 22:01:46 +08:00
SiriusXT
7f17f93767 Crop fileName and prevent cutting into the extension. 2024-11-01 21:43:09 +08:00
SiriusXT
cdd5a17fce Make note-detail-empty always display autocompletion. 2024-11-01 15:30:31 +08:00
SiriusXT
dbca50d9b0 Make note-detail-empty always display autocompletion. 2024-11-01 14:45:49 +08:00
Elian Doran
57a86c75d8 i18n: Fix single Romanian translation 2024-10-31 23:54:50 +02:00
Elian Doran
9e3c1b46cd client: Don't load syntax highlighter when not needed 2024-10-31 22:47:34 +02:00
Elian Doran
00209ec77a client: Apply syntax highlight to included notes 2024-10-31 22:18:00 +02:00
Elian Doran
dfa4f3cd84 client: Apply syntax highlight to note preview 2024-10-31 22:14:54 +02:00
Elian Doran
3af29a78dc client: Refactor syntax highlighting for read-only text into service 2024-10-31 22:11:59 +02:00
Elian Doran
4d783f1879 client: Fix color theme leak when deactivating highlighting 2024-10-31 21:45:06 +02:00
Elian Doran
c3e10b2b76 client: Remove syntax highlight in preview when disabled 2024-10-31 21:33:00 +02:00
Elian Doran
f57ab4b9f0 client: Fix word wrap preview being in reverse 2024-10-31 21:29:01 +02:00
Elian Doran
a690155d7e client: Improve group for no theme 2024-10-31 21:17:40 +02:00
Elian Doran
cc0b3db424 client: Translate dark/light color theme groups 2024-10-31 21:00:48 +02:00
Elian Doran
ae60f8c842 client: Group color themes by dark/light 2024-10-31 20:54:33 +02:00
Elian Doran
90dffdc6ed client: Enable preview for word wrap 2024-10-31 20:18:02 +02:00
Elian Doran
ac13291744 client,server: Allow disabling syntax highlight 2024-10-31 18:03:52 +02:00
Elian Doran
bbc038f254 Merge remote-tracking branch 'origin/develop' into feature/syntax_highlight 2024-10-31 17:48:49 +02:00
Elian Doran
f8df3a6933 client: Fix crash for some unhandled rejections 2024-10-31 17:48:33 +02:00
Elian Doran
b10e2d9ec4 Update README to add a few shields 2024-10-31 14:00:14 +02:00
SiriusXT
2387bbd17f Automatically trigger autocomplete on focus. 2024-10-30 22:30:40 +08:00
Adorian Doran
f13d88c3c0 Add a background color transition for the code sample 2024-10-29 18:46:55 +02:00
Adorian Doran
2459bbf341 Improve the layout of the "Word wrapping" checkbox 2024-10-29 18:39:14 +02:00
Adorian Doran
60426ea487 Fix word-wrapping 2024-10-29 12:57:15 +02:00
Adorian Doran
b112cb609f Tweak the padding of the language badges 2024-10-29 01:55:29 +02:00
Adorian Doran
b9ebc66122 Customize the scrollbar in code boxes for WebKit-based browsers 2024-10-29 01:30:08 +02:00
Adorian Doran
2f4ed92346 Prevent the language badge to be scrolled in code boxes 2024-10-29 01:07:24 +02:00
Adorian Doran
d3d001d8ea Tweak (again) the shadow of code blocks 2024-10-28 23:52:45 +02:00
Adorian Doran
70cee7dbf6 Tweak the shadow of code blocks 2024-10-28 23:44:40 +02:00
Adorian Doran
36fde2b03d Tweak the language badge of code blocks 2024-10-28 23:29:53 +02:00
Potjoe-97
88d8f57697 Merge pull request #3 from Potjoe-97/patch-2
Update translation.json
2024-10-28 16:03:33 +01:00
Potjoe-97
b7e254975f Update translation.json 2024-10-28 16:02:17 +01:00
Potjoe-97
97b2ba2da1 Update server.json 2024-10-28 16:01:04 +01:00
Adorian Doran
bda8173932 Improve the sample code 2024-10-28 16:08:46 +02:00
Adorian Doran
48f9f072b4 Format theme names 2024-10-28 16:07:52 +02:00
Elian Doran
9c55203ea0 client: Add credits 2024-10-28 00:05:43 +02:00
Elian Doran
dbb5e0e971 server: Add friendlier names for color themes 2024-10-27 23:46:03 +02:00
Elian Doran
b8eb09b46b server: Refactor code block theme search into own service 2024-10-27 23:12:55 +02:00
Elian Doran
5682b2d819 client: Translate word wrapping 2024-10-27 22:57:34 +02:00
Elian Doran
5109c07e9c client: Toggle word wrapping for code blocks 2024-10-27 22:51:24 +02:00
Elian Doran
b8569ea243 client, server: Create option to control word wrapping for code blocks 2024-10-27 21:51:56 +02:00
Elian Doran
52bc28def7 client: Rename section to CodeBlockOptions 2024-10-27 21:42:40 +02:00
Elian Doran
e65d4cdfbf client: Rename endpoint to codeblock-themes 2024-10-27 21:40:22 +02:00
Elian Doran
96b9042559 client: Rename option to codeBlockTheme 2024-10-27 21:39:50 +02:00
Elian Doran
e68d070320 client: Set up localization for syntax highlighting section 2024-10-27 21:27:35 +02:00
Elian Doran
ef5f2c680b client: Rephrase theme section 2024-10-27 21:19:27 +02:00
Elian Doran
6717b1b4ae client: Rephrase section 2024-10-27 21:15:51 +02:00
Elian Doran
41e3163595 client: Fix flicker of font selection 2024-10-27 21:03:13 +02:00
Elian Doran
514653fb50 client: Fix flicker of preview 2024-10-27 20:22:23 +02:00
Elian Doran
e843f1adc1 client: Fix background of preview 2024-10-27 20:19:53 +02:00
Elian Doran
83f5b47c99 client: Set up simple preview for syntax highlight 2024-10-27 20:18:44 +02:00
Elian Doran
2fdff29067 client: Apply syntax highlight in real-time 2024-10-27 20:08:12 +02:00
Elian Doran
0d270cbeb6 client: Use 3px shadow for dark theme 2024-10-27 20:01:08 +02:00
Elian Doran
f947a039b9 client: Apply background to read-only code blocks as well 2024-10-27 19:58:00 +02:00
Elian Doran
d2235a185b client: Improve style for code blocks 2024-10-27 19:54:05 +02:00
Elian Doran
87bc142552 client: Fix foreground color 2024-10-27 19:43:48 +02:00
Elian Doran
1a25f60264 client: Fix background color 2024-10-27 19:41:28 +02:00
Elian Doran
fe4dbae079 client: Apply highlighting theme on refresh 2024-10-27 17:41:37 +02:00
Elian Doran
e1ae014b74 server: Remove dashes from syntax theme name 2024-10-27 17:25:05 +02:00
Elian Doran
7952a5a81e client: Fix order of options 2024-10-27 12:54:40 +02:00
Elian Doran
60b6f7df89 client: Allow switching theme 2024-10-27 12:54:06 +02:00
Elian Doran
7354fb5b4a client,server: List syntax highlighting themes 2024-10-27 12:41:53 +02:00
Elian Doran
1fb0b74f76 client: Use same mechanism for read-only notes 2024-10-27 12:15:32 +02:00
Elian Doran
9e3b915612 client: Use translation for auto-detect 2024-10-27 11:47:36 +02:00
Elian Doran
7505db220e client: Implement auto syntax highlighting 2024-10-27 11:46:19 +02:00
Elian Doran
a3932376f3 client: Add Javadoc for newly introduced methods 2024-10-27 11:32:54 +02:00
Elian Doran
3a609d54ab client: Fix highlighting for JavaScript 2024-10-27 11:21:08 +02:00
Elian Doran
c4bd4eb440 client: Respect user language selection for editor 2024-10-27 11:18:36 +02:00
Elian Doran
e931df721d client: Fix duplication when requesting scripts 2024-10-27 10:48:50 +02:00
Elian Doran
1e9324c303 client: Support custom language types for highlight 2024-10-27 10:39:31 +02:00
Elian Doran
6c4513fb2e client: Enable syntax highlight for read-only notes 2024-10-27 08:52:34 +02:00
Elian Doran
c7e1362105 Merge branch 'develop' into feature/syntax_highlight 2024-10-26 23:39:49 +03:00
Elian Doran
acf37f9327 client: Fix error when duplicating note 2024-10-26 23:39:38 +03:00
Elian Doran
f80cf0aa02 Add limit to blocks highlighting 2024-10-26 23:39:18 +03:00
Elian Doran
6078620bf1 Carry over code block highlighting 2024-10-26 23:27:23 +03:00
Elian Doran
579b3f4ca0 Carry over highlighter initialization 2024-10-26 23:21:51 +03:00
Elian Doran
bf28005f46 Create dedicated file for syntax highlight 2024-10-26 23:16:24 +03:00
Elian Doran
c81b847b61 Set up highlight.js 2024-10-26 22:57:07 +03:00
Elian Doran
88cd2ac25c build: Fix duplication 2024-10-26 01:00:44 +03:00
Elian Doran
e3e6f56a88 build: Add icon.png for Linux builds (fixes #507) 2024-10-26 00:58:02 +03:00
Elian Doran
0768a2a0a3 build: Add StartupWMClass to deb build 2024-10-26 00:42:44 +03:00
Elian Doran
84d216da54 i18n: Translate missing keys for Romanian 2024-10-25 21:06:03 +03:00
Elian Doran
391f518c01 i18n: Translate search note prefix 2024-10-25 21:04:13 +03:00
Elian Doran
2324c9a13b client: Fix HTML in some toasts 2024-10-25 20:51:50 +03:00
Elian Doran
6799c44e22 client: Fix redundant toast message 2024-10-25 20:50:13 +03:00
Elian Doran
f0052d56b7 Merge pull request #520 from hasecilu/i18n/more_Spanish_translation
More Spanish translation
2024-10-25 20:49:48 +03:00
Elian Doran
53822fd47f client: Remove redundant global 2024-10-25 20:44:08 +03:00
hasecilu
b02c4b54e5 i18n: Fix source strings 2024-10-25 11:32:43 -06:00
hasecilu
27f07ee604 i18n: Update Spanish translations 2024-10-25 11:32:42 -06:00
Elian Doran
03a23d15f9 client: Fix double errors if not returning a widget 2024-10-25 20:22:29 +03:00
Elian Doran
70d55097ee client: Fix crash if note tree fails to find a child note 2024-10-25 20:15:12 +03:00
Elian Doran
560467bdba client: Log uncaught promise errors 2024-10-25 19:57:40 +03:00
Elian Doran
cb4fe4481f client: Strengthen widget rendering errors detection 2024-10-25 19:57:31 +03:00
Elian Doran
eee088316d client: Improve logging for some bundle errors 2024-10-24 20:55:36 +03:00
Elian Doran
81ca0a3776 client: Improve logging for basic sync crash 2024-10-24 18:47:16 +03:00
Elian Doran
48b0af1bba client: Stop crash if right widget crashes during render 2024-10-24 18:14:17 +03:00
Elian Doran
43ef452d44 client: Fix error when running script due to translations 2024-10-23 20:33:55 +03:00
Elian Doran
70ebf1a08f client: Fix content size for code editor 2024-10-23 20:27:36 +03:00
Elian Doran
9f6f0f5d60 server: Update locale when switching language from settings 2024-10-23 19:56:06 +03:00
Elian Doran
af67362ad6 server: Translate weekday and month names 2024-10-23 19:34:09 +03:00
Elian Doran
77550f3087 server: Fix regression due to express types 2024-10-22 20:09:42 +03:00
Elian Doran
5813282248 Prepare for 0.90.9-beta 2024-10-22 20:05:43 +03:00
Elian Doran
e77b223508 client: Update force-graph 1.43.5 -> 1.45.0 2024-10-22 20:03:05 +03:00
Elian Doran
7aafdce629 server: Update jasmine, debounce 2024-10-22 19:56:15 +03:00
Elian Doran
a2f0cb394a server: Update marked, sanitize-html to latest 2024-10-22 19:53:22 +03:00
Elian Doran
e8d1518965 build: Update TypeScript 2024-10-22 19:30:34 +03:00
Elian Doran
8b333b32af mind-elixir: 4.2.2 -> 4.2.3 2024-10-22 19:27:53 +03:00
Elian Doran
cda369ed4d server: Update express, express-rate-limit, express-session to latest 2024-10-22 19:25:47 +03:00
Elian Doran
b5bc93d794 i18next: 23.16.1 -> 23.16.2 2024-10-22 19:24:16 +03:00
Elian Doran
b96047e962 vanilla-js-wheel-zoom: 9.0.2 -> 9.0.4 2024-10-22 19:22:14 +03:00
Elian Doran
31ccbb0d23 mind-elixir: 4.2.0 -> 4.2.2 2024-10-22 19:17:52 +03:00
Elian Doran
cb9403535d i18next: 23.16.0 -> 23.16.1 2024-10-22 19:17:52 +03:00
Elian Doran
b5ee90a1d2 i18n: Translate delete/restore branch 2024-10-22 19:17:52 +03:00
Elian Doran
9ed7eb977e i18n: Translate launcher context menu 2024-10-22 19:17:52 +03:00
Elian Doran
8cc487da7c i18n: Translate confirmation popups 2024-10-22 19:17:52 +03:00
Elian Doran
ae593ea363 i18n: Translate protected session 2024-10-22 19:17:52 +03:00
Elian Doran
26e4decaec i18n: Translate toast errors 2024-10-22 19:17:52 +03:00
Elian Doran
28f6712a4f i18n: Translate toast messages 2024-10-22 19:17:52 +03:00
Elian Doran
93efce4023 server: Minimize not found logs (closes #505) 2024-10-22 19:17:52 +03:00
Elian Doran
689b3a3079 i18n: Fix capitalization of no anonymization 2024-10-22 19:17:51 +03:00
Elian Doran
4ad725842e server: Trim .htm when importing zip (closes #500) 2024-10-20 00:17:51 +03:00
Elian Doran
d4956ad3a2 client: Refactor and add documentation 2024-10-19 23:19:11 +03:00
Elian Doran
c7b7c68a05 client: Reduce code duplication for CodeMirror 2024-10-19 23:12:33 +03:00
Elian Doran
cab1d7d353 client: Set up syntax highlight in read-only code (closes #504) 2024-10-19 22:56:45 +03:00
Elian Doran
7957c6d34e client: Fix promoted attribute style regressions (closes #503) 2024-10-19 22:40:27 +03:00
Elian Doran
c18c972a57 i18n: Use variable interpolation for delete relation warning 2024-10-19 11:13:54 +03:00
Elian Doran
29a700f731 i18n: Fix duplication in delete relations count 2024-10-19 10:55:48 +03:00
Elian Doran
815eab26f6 i18n: Fix duplication in delete note count 2024-10-19 10:50:56 +03:00
Elian Doran
103da23b5a i18n: Fix strange title in Romanian 2024-10-19 10:46:28 +03:00
Elian Doran
ba1d82bc0a i18n: Fix capitalization of checkbox 2024-10-19 10:45:45 +03:00
Elian Doran
21f8a29761 Bump to v0.90.8 2024-10-19 09:44:34 +03:00
Elian Doran
f38870b27d i18next: 23.15.2 -> 23.16.0 2024-10-17 23:15:46 +03:00
Elian Doran
56a6d27240 client: mind-elixir: 4.1.5 -> 4.2.0 2024-10-17 23:12:43 +03:00
Elian Doran
38e5ef2c7d i18n: Translate some more Romanian messages 2024-10-17 22:50:20 +03:00
Elian Doran
e29d600517 Merge pull request #489 from TriliumNext/add-stable-tag-to-containers
Introduce `stable` tag on containers
2024-10-17 22:43:04 +03:00
Elian Doran
42605fbbad Merge pull request #495 from Potjoe-97/develop
i18n : Add fr translation (2/2)
2024-10-17 22:42:01 +03:00
Elian Doran
11ca427a28 Merge pull request #496 from hasecilu/i18n/Spanish_more
Continue Spanish translation
2024-10-17 22:40:34 +03:00
hasecilu
28d8088763 i18n: Create script to create PO files for translation
Script for translators
2024-10-17 13:34:40 -06:00
hasecilu
664c4789c0 i18n: Update Spanish translation 2024-10-17 12:30:36 -06:00
Potjoe-97
7c5667b457 Minor fixes 2024-10-17 15:54:42 +02:00
Potjoe-97
0afd22e196 Edited French option to display native spelling 2024-10-17 15:12:51 +02:00
Potjoe-97
b3abee71b7 Major overhaul fr translation
Corrections & Consistency
2024-10-17 13:33:28 +02:00
Elian Doran
9bd5596b2a i18n: Set up French 2024-10-16 20:29:42 +03:00
Elian Doran
e0e3c15e6e Merge pull request #493 from Potjoe-97/develop
Add french translation
2024-10-16 20:28:38 +03:00
Potjoe-97
31396264fa Corrections i18n : {} attribute detail 2024-10-16 14:52:05 +02:00
Potjoe-97
b1aada22b5 Corrections in /src/public/translations/fr 2024-10-16 11:32:34 +02:00
Elian Doran
d7eaf72a6d Merge pull request #491 from TriliumNext/feature/i18n-part6
Feature/i18n part6
2024-10-16 12:13:28 +03:00
Potjoe-97
59df442676 Corrections /src/public/translations/fr
Consistency in translations/server.json
2024-10-15 20:29:34 +02:00
Potjoe-97
9770db7f3c Consistency : "étiquette" now translated into "label" 2024-10-15 18:52:34 +02:00
Potjoe-97
8c36cea71b src/public/translations/fr : First draft 2024-10-15 18:16:39 +02:00
Potjoe-97
b03f40f1f9 Edit french translation 2024-10-15 15:48:11 +02:00
Potjoe-97
00dba7bef4 Add french translation 2024-10-15 15:20:04 +02:00
Nriver
4186f3d136 add translation for app_context.js 2024-10-15 15:46:34 +08:00
Nriver
529502524d add missing context menu translation 2024-10-15 15:24:01 +08:00
Nriver
7c518e9512 add translation for watched_file_update_status.js 2024-10-15 15:19:09 +08:00
Nriver
5e2d1bc124 add translation for toc.js 2024-10-15 15:12:09 +08:00
Nriver
7dfe6f276e update Chinese translation and synchronize with English 2024-10-15 15:05:48 +08:00
Nriver
858db68d66 add translation for tab_row.js 2024-10-15 14:51:26 +08:00
Jon Fuller
b72f46f108 Don't add stable if pushed tag has - 2024-10-14 14:06:55 -07:00
Elian Doran
83dbe0539e client: Highlight content links on hover 2024-10-14 22:47:16 +03:00
Jon Fuller
87e0cf55f1 Introduce stable tag on containers
Closes #488
2024-10-14 12:18:29 -07:00
Elian Doran
8315d5c778 Update package-lock 2024-10-14 22:13:04 +03:00
Elian Doran
61bd7dca18 client: Fix underlines for all links (closes #485) 2024-10-14 22:12:48 +03:00
Elian Doran
7f338044b0 Merge pull request #484 from meichthys/develop
Move Description section to top of bug report
2024-10-14 09:39:56 +03:00
meichthys
ea3f47b8fa Move Description section to top of bug report 2024-10-14 01:12:43 -04:00
141 changed files with 8030 additions and 1507 deletions

View File

@@ -3,6 +3,12 @@ description: Report a bug
title: "(Bug report) "
labels: "Type: Bug"
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug and any additional information.
validations:
required: true
- type: input
attributes:
label: TriliumNext Version
@@ -38,12 +44,6 @@ body:
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug and any additional information.
validations:
required: true
- type: textarea
attributes:
label: Error logs

View File

@@ -9,6 +9,12 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/notes
TEST_TAG: ${{ github.repository_owner }}/notes:test
jobs:
build_docker:
name: Build Docker image
@@ -30,4 +36,66 @@ jobs:
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: type=gha,mode=max
test_docker:
name: Check Docker build
runs-on: ubuntu-latest
strategy:
matrix:
include:
- dockerfile: Dockerfile.alpine
- dockerfile: Dockerfile
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Set IMAGE_NAME to lowercase
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Run the TypeScript build
run: npx tsc
- name: Create server-package.json
run: cat package.json | grep -v electron > server-package.json
- name: Build and export to Docker
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Validate container run output
run: |
CONTAINER_ID=$(docker run -d --log-driver=journald --rm --name trilium_local ${{ env.TEST_TAG }})
echo "Container ID: $CONTAINER_ID"
- name: Wait for the healthchecks to pass
uses: stringbean/docker-healthcheck-action@v1
with:
container: trilium_local
wait-time: 50
require-status: running
require-healthy: true
# Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded
- name: Print entire log
if: always()
run: |
journalctl -u docker CONTAINER_NAME=trilium_local --no-pager

View File

@@ -129,6 +129,8 @@ jobs:
type=ref,event=branch
type=ref,event=tag
type=sha
flavor: |
latest=false
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -213,7 +215,9 @@ jobs:
images: |
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
flavor: |
latest=false
- name: Login to GHCR
uses: docker/login-action@v2
with:
@@ -242,6 +246,32 @@ jobs:
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.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...
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
# First create stable tags
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
# Small delay to ensure stable tag is fully propagated
sleep 5
# Now update latest tags
docker buildx imagetools create \
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
docker buildx imagetools create \
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
fi
- name: Inspect image
run: |

View File

@@ -43,7 +43,7 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Update build info
run: npm run update-build-info
run: npm run update-build-info
- name: Run electron-forge
run: npm run make-electron -- --arch=${{ matrix.arch }}
- name: Prepare artifacts (Unix)

View File

@@ -41,6 +41,8 @@ jobs:
run: npm ci
- name: Update build info
run: npm run update-build-info
- name: Update nightly version
run: npm run ci-update-nightly-version
- name: Run electron-forge
run: npm run make-electron -- --arch=${{ matrix.arch }}
- name: Prepare artifacts (Unix)
@@ -103,6 +105,7 @@ jobs:
- name: Run Linux server build (x86_64)
run: |
npm run update-build-info
npm run ci-update-nightly-version
./bin/build-server.sh
- name: Prepare artifacts
if: runner.os != 'windows'

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ build/
src/public/app-dist/
npm-debug.log
yarn-error.log
po-*/
*.db
!integration-tests/db/document.db

View File

@@ -5,3 +5,16 @@ reviews:
description: >-
Describes the shortcut which triggers a search within the current
page/note only
add_label.to_value:
locales:
fr:
comments:
- user:
name: Potjoe-97
email: giann@LAPTOPT490-GF
id: QXec0JUoxfGmMlpch-B1S
comment: ''
suggestion: vers la valeur
type: request_change
time: '2024-10-15T16:57:06.188Z'
resolved: true

View File

@@ -1,7 +1,7 @@
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
FROM node:20.15.1-bullseye-slim
# Build stage
FROM node:20.15.1-bullseye-slim AS builder
# Configure system dependencies
# Configure build dependencies in a single layer
RUN apt-get update && apt-get install -y --no-install-recommends \
autoconf \
automake \
@@ -12,49 +12,52 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
nasm \
libpng-dev \
python3 \
gosu \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /usr/src/app
# Bundle app source
# Copy only necessary files for build
COPY . .
COPY server-package.json package.json
# Copy TypeScript build artifacts into the original directory structure.
# Copy the healthcheck
# Build and cleanup in a single layer
RUN cp -R build/src/* src/. && \
cp build/docker_healthcheck.js . && \
rm -r build && \
rm docker_healthcheck.ts
# Install app dependencies
RUN apt-get purge -y --auto-remove \
autoconf \
automake \
g++ \
gcc \
libtool \
make \
nasm \
libpng-dev \
python3 \
&& rm -rf /var/lib/apt/lists/*
RUN npm install && \
rm docker_healthcheck.ts && \
npm install && \
npm run webpack && \
npm prune --omit=dev
RUN cp src/public/app/share.js src/public/app-dist/. && \
npm prune --omit=dev && \
npm cache clean --force && \
cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app && rm src/services/asset_path.ts
rm -rf src/public/app && \
rm src/services/asset_path.ts
# Some setup tools need to be kept
# Runtime stage
FROM node:20.15.1-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gosu \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* && \
rm -rf /var/cache/apt/*
# Start the application
WORKDIR /usr/src/app
# Copy only necessary files from builder
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/src ./src
COPY --from=builder /usr/src/app/db ./db
COPY --from=builder /usr/src/app/docker_healthcheck.js .
COPY --from=builder /usr/src/app/start-docker.sh .
COPY --from=builder /usr/src/app/package.json .
COPY --from=builder /usr/src/app/config-sample.ini .
COPY --from=builder /usr/src/app/images ./images
COPY --from=builder /usr/src/app/translations ./translations
COPY --from=builder /usr/src/app/libraries ./libraries
# Configure container
EXPOSE 8080
CMD [ "./start-docker.sh" ]
HEALTHCHECK --start-period=10s CMD exec gosu node node docker_healthcheck.js

View File

@@ -1,7 +1,7 @@
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
FROM node:20.15.1-alpine
# Build stage
FROM node:20.15.1-alpine AS builder
# Configure system dependencies
# Configure build dependencies
RUN apk add --no-cache --virtual .build-dependencies \
autoconf \
automake \
@@ -11,43 +11,52 @@ RUN apk add --no-cache --virtual .build-dependencies \
make \
nasm \
libpng-dev \
python3
python3
# Create app directory
WORKDIR /usr/src/app
# Bundle app source
# Copy only necessary files for build
COPY . .
COPY server-package.json package.json
# Copy TypeScript build artifacts into the original directory structure.
# Copy the healthcheck
# Build and cleanup in a single layer
RUN cp -R build/src/* src/. && \
cp build/docker_healthcheck.js . && \
rm -r build && \
rm docker_healthcheck.ts
# Install app dependencies
RUN set -x && \
rm docker_healthcheck.ts && \
npm install && \
apk del .build-dependencies && \
npm run webpack && \
npm prune --omit=dev && \
npm cache clean --force && \
cp src/public/app/share.js src/public/app-dist/. && \
cp -r src/public/app/doc_notes src/public/app-dist/. && \
rm -rf src/public/app && \
rm src/services/asset_path.ts
# Runtime stage
FROM node:20.15.1-alpine
# Some setup tools need to be kept
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow
# Add application user and setup proper volume permissions
WORKDIR /usr/src/app
# Copy only necessary files from builder
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/src ./src
COPY --from=builder /usr/src/app/db ./db
COPY --from=builder /usr/src/app/docker_healthcheck.js .
COPY --from=builder /usr/src/app/start-docker.sh .
COPY --from=builder /usr/src/app/package.json .
COPY --from=builder /usr/src/app/config-sample.ini .
COPY --from=builder /usr/src/app/images ./images
COPY --from=builder /usr/src/app/translations ./translations
COPY --from=builder /usr/src/app/libraries ./libraries
# Add application user
RUN adduser -s /bin/false node; exit 0
# Start the application
# Configure container
EXPOSE 8080
CMD [ "./start-docker.sh" ]
HEALTHCHECK --start-period=10s CMD exec su-exec node node docker_healthcheck.js
HEALTHCHECK --start-period=10s CMD exec su-exec node node docker_healthcheck.js

View File

@@ -1,5 +1,7 @@
# TriliumNext Notes
![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/triliumnext/notes/total)
[English](./README.md) | [Chinese](./README-ZH_CN.md) | [Russian](./README.ru.md) | [Japanese](./README.ja.md) | [Italian](./README.it.md) | [Spanish](./README.es.md)
TriliumNext Notes is an open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
@@ -16,6 +18,8 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Just upgrade your Trilium instance to the latest version and [install TriliumNext/Notes as usual](#-installation)
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
## 💬 Discuss with us
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
@@ -63,6 +67,16 @@ To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have
* Currently only the latest versions of Chrome & Firefox are supported (and tested).
* (Coming Soon) TriliumNext will also be provided as a Flatpak
#### MacOS
Currently when running TriliumNext/Notes on MacOS, you may get the following error:
> Apple could not verify "TriliumNext Notes" is free of malware and may harm your Mac or compromise your privacy.
You will need to run the command on your shell to resolve the error (documented [here](https://github.com/TriliumNext/Notes/issues/329#issuecomment-2287164137)):
```bash
xattr -c "/path/to/Trilium Next.app"
```
### Mobile
To use TriliumNext on a mobile device:

View File

@@ -47,6 +47,15 @@ const copy = async () => {
await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir)));
}
/**
* Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist.
*/
const publicDirsToCopy = [ "./src/public/app/doc_notes" ];
const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist");
for (const dir of publicDirsToCopy) {
await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir)));
}
const nodeModulesFile = [
"node_modules/react/umd/react.production.min.js",
"node_modules/react/umd/react.development.js",
@@ -55,6 +64,7 @@ const copy = async () => {
"node_modules/katex/dist/katex.min.js",
"node_modules/katex/dist/contrib/mhchem.min.js",
"node_modules/katex/dist/contrib/auto-render.min.js",
"node_modules/@highlightjs/cdn-assets/highlight.min.js"
];
for (const file of nodeModulesFile) {
@@ -89,7 +99,9 @@ const copy = async () => {
"node_modules/codemirror/addon/",
"node_modules/codemirror/mode/",
"node_modules/codemirror/keymap/",
"node_modules/mind-elixir/dist/"
"node_modules/mind-elixir/dist/",
"node_modules/@highlightjs/cdn-assets/languages",
"node_modules/@highlightjs/cdn-assets/styles"
];
for (const folder of nodeModulesFolder) {

View File

@@ -1,15 +0,0 @@
{
"src": "dist/trilium-linux-x64",
"dest": "dist/",
"compression": "xz",
"name": "trilium",
"productName": "Trilium Notes",
"genericName": "Note taker",
"description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.",
"sections": "misc",
"maintainer": "zadam.apps@gmail.com",
"homepage": "https://github.com/zadam/trilium",
"bin": "trilium",
"icon": "dist/trilium-linux-x64/icon.png",
"categories": [ "Office" ]
}

View File

@@ -0,0 +1,12 @@
[Desktop Entry]
<% if (productName) { %>Name=<%= productName %>
<% } %><% if (description) { %>Comment=<%= description %>
<% } %><% if (genericName) { %>GenericName=<%= genericName %>
<% } %><% if (name) { %>Exec=<%= name %> %U
Icon=<%= name %>
<% } %>Type=Application
StartupNotify=true
<% if (productName) { %>StartupWMClass=<%= productName %>
<% } if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;
<% } %><% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;
<% } %>

110
bin/translation.sh Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
# --------------------------------------------------------------------------------------------------
#
# Create PO files to make easier the labor of translation.
#
# Info:
# https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
# https://docs.translatehouse.org/projects/translate-toolkit/en/latest/commands/json2po.html
#
# Dependencies:
# jq
# translate-toolkit
# python-wcwidth
#
# Created by @hasecilu
#
# --------------------------------------------------------------------------------------------------
number_of_keys() {
[ -f "$1" ] && jq 'path(..) | select(length == 2) | .[1]' "$1" | wc -l || echo "0"
}
stats() {
# Print the number of existing strings on the JSON files for each locale
s=$(number_of_keys "${paths[0]}/en/server.json")
c=$(number_of_keys "${paths[1]}/en/translation.json")
echo "| locale |server strings |client strings |"
echo "|--------|---------------|---------------|"
echo "| en | ${s} | ${c} |"
for locale in "${locales[@]}"; do
s=$(number_of_keys "${paths[0]}/${locale}/server.json")
c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
n1=$(((8 - ${#locale}) / 2))
n2=$((n1 == 1 ? n1 + 1 : n1))
echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |"
done
}
update_1() {
# Update PO files from English and localized JSON files as source
# NOTE: if you want a new language you need to first create the JSON files
# on their corresponding place with `{}` as content to avoid error on `json2po`
local locales=("$@")
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
done
}
update_2() {
# Recover translation from PO files to localized JSON files
local locales=("$@")
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
done
}
help() {
echo -e "\nDescription:"
echo -e "\tCreate PO files to make easier the labor of translation"
echo -e "\nUsage:"
echo -e "\t./translation.sh [--stats] [--update1 <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
echo -e "\nFlags:"
echo -e " --clear\n\tClear all po-* directories"
echo -e " --stats\n\tPrint the number of existing strings on the JSON files for each locale"
echo -e " --update1 <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
echo -e " --update2 <LOCALE>\n\tRecover translation from PO files to localized JSON files"
}
# Main function ------------------------------------------------------------------------------------
# Get script directory to set file path relative to it
file_path="$(
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
pwd -P
)"
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
locales=(cn de es fr pt_br ro tw)
if [ $# -eq 1 ]; then
if [ "$1" == "--clear" ]; then
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
[ -d "${path}/po-${locale}" ] && rm -r "${path}/po-${locale}"
done
done
elif [ "$1" == "--stats" ]; then
stats
elif [ "$1" == "--update1" ]; then
update_1 "${locales[@]}"
elif [ "$1" == "--update2" ]; then
update_2 "${locales[@]}"
else
help
fi
elif [ $# -eq 2 ]; then
if [ "$1" == "--update1" ]; then
update_1 "$2"
elif [ "$1" == "--update2" ]; then
update_2 "$2"
else
help
fi
else
help
fi

View File

@@ -0,0 +1,50 @@
/**
* @module
*
* The nightly version works uses the version described in `package.json`, just like any release.
* The problem with this approach is that production builds have a very aggressive cache, and
* usually running the nightly with this cached version of the application will mean that the
* user might run into module not found errors or styling errors caused by an old cache.
*
* This script is supposed to be run in the CI, which will update locally the version field of
* `package.json` to contain the date. For example, `0.90.9-beta` will become `0.90.9-test-YYMMDD-HHMMSS`.
*
*/
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import fs from "fs";
function processVersion(version) {
// Remove the beta suffix if any.
version = version.replace("-beta", "");
// Add the nightly suffix, plus the date.
const referenceDate = new Date()
.toISOString()
.substring(2, 19)
.replace(/[-:]*/g, "")
.replace("T", "-");
version = `${version}-test-${referenceDate}`;
return version;
}
function main() {
const scriptDir = dirname(fileURLToPath(import.meta.url));
const packageJsonPath = join(scriptDir, "..", "package.json");
// Read the version from package.json and process it.
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
const currentVersion = packageJson.version;
const adjustedVersion = processVersion(currentVersion);
console.log("Current version is", currentVersion);
console.log("Adjusted version is", adjustedVersion);
// Write the adjusted version back in.
packageJson.version = adjustedVersion;
const formattedJson = JSON.stringify(packageJson, null, 4);
fs.writeFileSync(packageJsonPath, formattedJson);
}
main();

Binary file not shown.

View File

@@ -15,18 +15,26 @@ module.exports = {
...getExtraResourcesForPlatform(),
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
"translations/"
"translations/",
"node_modules/@highlightjs/cdn-assets/styles"
],
afterComplete: [(buildPath, _electronVersion, platform, _arch, callback) => {
const extraResources = getExtraResourcesForPlatform();
for (const resource of extraResources) {
const baseName = path.basename(resource);
let sourcePath;
if (platform === 'darwin') {
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', path.basename(resource));
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', baseName);
} else {
sourcePath = path.join(buildPath, 'resources', path.basename(resource));
sourcePath = path.join(buildPath, 'resources', baseName);
}
let destPath;
if (baseName !== "256x256.png") {
destPath = path.join(buildPath, baseName);
} else {
destPath = path.join(buildPath, "icon.png");
}
const destPath = path.join(buildPath, path.basename(resource));
// Copy files from resources folder to root
fs.move(sourcePath, destPath)
@@ -44,6 +52,7 @@ module.exports = {
config: {
options: {
icon: "./images/app-icons/png/128x128.png",
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
}
}
},
@@ -95,6 +104,7 @@ function getExtraResourcesForPlatform() {
case 'darwin':
break;
case 'linux':
resources.push("images/app-icons/png/256x256.png")
for (const script of scripts) {
resources.push(`./bin/tpl/${script}.sh`)
}

View File

@@ -526,16 +526,19 @@
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
border: 0px;
border-radius: 6px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
}
.ck-content pre:not(.hljs) {
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {

49
libraries/ckeditor/ckeditor.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { DecoupledEditor as DecoupledEditorBase } from '@ckeditor/ckeditor5-editor-decoupled';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Alignment } from '@ckeditor/ckeditor5-alignment';
import { FontSize, FontFamily, FontColor, FontBackgroundColor } from '@ckeditor/ckeditor5-font';
import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
import { Link } from '@ckeditor/ckeditor5-link';
import { List, ListProperties } from '@ckeditor/ckeditor5-list';
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
export default class DecoupledEditor extends DecoupledEditorBase {
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof Alignment | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof CKFinderUploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof Strikethrough | typeof Underline | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof ListProperties | typeof Indent | typeof IndentBlock | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
static defaultConfig: {
toolbar: {
items: string[];
};
image: {
resizeUnit: "px";
toolbar: string[];
};
table: {
contentToolbar: string[];
};
list: {
properties: {
styles: boolean;
startIndex: boolean;
reversed: boolean;
};
};
language: string;
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1084
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "trilium",
"productName": "TriliumNext Notes",
"description": "Build your personal knowledge base with TriliumNext Notes",
"version": "0.90.7-beta",
"version": "0.90.12",
"license": "AGPL-3.0-only",
"main": "./dist/electron-main.js",
"author": {
@@ -46,12 +46,14 @@
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000"
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts"
},
"dependencies": {
"@braintree/sanitize-url": "7.1.0",
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.10.0",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@@ -63,30 +65,30 @@
"cls-hooked": "4.2.2",
"codemirror": "5.65.18",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"cookie-parser": "1.4.7",
"csurf": "1.11.0",
"dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2",
"debounce": "2.1.1",
"debounce": "2.2.0",
"ejs": "3.1.10",
"electron-debug": "4.0.1",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"eslint": "9.10.0",
"express": "4.21.0",
"eslint": "9.14.0",
"express": "4.21.1",
"express-partial-content": "1.0.2",
"express-rate-limit": "7.4.0",
"express-session": "1.18.0",
"force-graph": "1.43.5",
"express-rate-limit": "7.4.1",
"express-session": "1.18.1",
"force-graph": "1.46.0",
"fs-extra": "11.2.0",
"helmet": "7.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.5",
"i18next": "23.15.2",
"i18next": "23.16.4",
"i18next-fs-backend": "2.3.2",
"i18next-http-backend": "2.6.2",
"image-type": "4.1.0",
@@ -103,10 +105,10 @@
"katex": "0.16.11",
"knockout": "3.5.1",
"mark.js": "8.11.1",
"marked": "14.1.2",
"mermaid": "11.3.0",
"marked": "14.1.3",
"mermaid": "11.4.0",
"mime-types": "2.1.35",
"mind-elixir": "4.1.5",
"mind-elixir": "4.3.1",
"multer": "1.4.5-lts.1",
"node-abi": "3.67.0",
"normalize-strings": "1.1.1",
@@ -119,7 +121,7 @@
"request": "2.88.2",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.13.0",
"sanitize-html": "2.13.1",
"sax": "1.4.1",
"semver": "7.6.3",
"serve-favicon": "2.5.0",
@@ -132,7 +134,7 @@
"tree-kill": "1.2.2",
"turndown": "7.2.0",
"unescape": "1.0.1",
"vanilla-js-wheel-zoom": "9.0.2",
"vanilla-js-wheel-zoom": "9.0.4",
"ws": "8.18.0",
"xml2js": "0.6.2",
"yauzl": "3.1.3"
@@ -144,7 +146,7 @@
"@electron-forge/maker-squirrel": "7.5.0",
"@electron-forge/maker-zip": "7.5.0",
"@electron-forge/plugin-auto-unpack-natives": "7.5.0",
"@playwright/test": "1.47.1",
"@playwright/test": "1.48.2",
"@types/archiver": "6.0.2",
"@types/better-sqlite3": "7.6.11",
"@types/cls-hooked": "4.3.8",
@@ -163,7 +165,7 @@
"@types/jsdom": "21.1.7",
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/node": "22.5.4",
"@types/node": "22.7.8",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.13.0",
"@types/sax": "1.2.7",
@@ -182,17 +184,17 @@
"electron-rebuild": "3.2.9",
"esm": "3.2.25",
"iconsur": "1.7.0",
"jasmine": "5.3.1",
"jasmine": "5.4.0",
"jsdoc": "4.0.3",
"lorem-ipsum": "2.0.8",
"nodemon": "3.1.7",
"rcedit": "4.0.1",
"rimraf": "6.0.1",
"ts-node": "10.9.2",
"tslib": "2.7.0",
"tsx": "4.19.1",
"tslib": "2.8.1",
"tsx": "4.19.2",
"typescript": "5.6.3",
"webpack": "5.95.0",
"webpack": "5.96.1",
"webpack-cli": "5.1.4"
}
}

10
renovate.json Normal file
View File

@@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"repositories": ["TriliumNext/Notes"],
"schedule": ["before 3am"],
"labels": ["dependencies", "renovate"],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"branchConcurrentLimit": 0
}

View File

@@ -38,9 +38,18 @@ export interface RecentNoteRow {
utcDateCreated?: string;
}
/**
* Database representation of an option.
*
* Options are key-value pairs that are used to store information such as user preferences (for example
* the current theme, sync server information), but also information about the state of the application).
*/
export interface OptionRow {
/** The name of the option. */
name: string;
/** The value of the option. */
value: string;
/** `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme). */
isSynced: boolean;
utcDateModified?: string;
}

View File

@@ -13,7 +13,7 @@ import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js";
import MainTreeExecutors from "./main_tree_executors.js";
import toast from "../services/toast.js";
import ShortcutComponent from "./shortcut_component.js";
import { initLocale } from "../services/i18n.js";
import { t, initLocale } from "../services/i18n.js";
class AppContext extends Component {
constructor(isMainWindow) {
@@ -33,11 +33,11 @@ class AppContext extends Component {
await initLocale();
}
setLayout(layout) {
setLayout(layout) {
this.layout = layout;
}
async start() {
async start() {
this.initComponents();
this.renderWidgets();
@@ -151,7 +151,7 @@ $(window).on('beforeunload', () => {
if (!component.beforeUnloadEvent()) {
console.log(`Component ${component.componentId} is not finished saving its state.`);
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
allSaved = false;
}

View File

@@ -9,6 +9,7 @@ import ws from "../services/ws.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import linkService from "../services/link.js";
import { t } from "../services/i18n.js";
export default class Entrypoints extends Component {
constructor() {
@@ -172,13 +173,13 @@ export default class Entrypoints extends Component {
const resp = await server.post(`sql/execute/${note.noteId}`);
if (!resp.success) {
toastService.showError(`Error occurred while executing SQL query: ${resp.error}`);
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
}
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
}
toastService.showMessage("Note executed");
toastService.showMessage(t("entrypoints.note-executed"));
}
hideAllPopups() {
@@ -200,6 +201,6 @@ export default class Entrypoints extends Component {
await server.post(`notes/${noteId}/revision`);
toastService.showMessage("Note revision has been created.");
toastService.showMessage(t("entrypoints.note-revision-created"));
}
}

View File

@@ -551,7 +551,7 @@ export default class TabManager extends Component {
await this.removeNoteContext(ntxIdToRemove);
}
}
async closeOtherTabsCommand({ntxId}) {
for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) {
if (ntxIdToRemove !== ntxId) {
@@ -560,6 +560,18 @@ export default class TabManager extends Component {
}
}
async closeRightTabsCommand({ntxId}) {
const ntxIds = this.mainNoteContexts.map(nc => nc.ntxId);
const index = ntxIds.indexOf(ntxId);
if (index !== -1) {
const idsToRemove = ntxIds.slice(index + 1);
for (const ntxIdToRemove of idsToRemove) {
await this.removeNoteContext(ntxIdToRemove);
}
}
}
async closeTabCommand({ntxId}) {
await this.removeNoteContext(ntxId);
}
@@ -574,6 +586,11 @@ export default class TabManager extends Component {
}
}
async copyTabToNewWindowCommand({ntxId}) {
const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
}
async reopenLastTabCommand() {
let closeLastEmptyTab = null;

View File

@@ -16,7 +16,7 @@ class ZoomComponent extends Component {
window.addEventListener("wheel", event => {
if (event.ctrlKey) {
this.setZoomFactorAndSave(this.getCurrentZoom() + event.deltaY * 0.001);
this.setZoomFactorAndSave(this.getCurrentZoom() - event.deltaY * 0.001);
}
});
}
@@ -56,7 +56,7 @@ class ZoomComponent extends Component {
zoomResetEvent() {
this.setZoomFactorAndSave(1);
}
setZoomFactorAndSaveEvent({zoomFactor}) {
this.setZoomFactorAndSave(zoomFactor);
}

View File

@@ -9,9 +9,9 @@ import electronContextMenu from "./menus/electron_context_menu.js";
import glob from "./services/glob.js";
import { t } from "./services/i18n.js";
await appContext.earlyInit();
bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
await appContext.earlyInit();
// A dynamic import is required for layouts since they initialize components which require translations.
const DesktopLayout = (await import("./layouts/desktop_layout.js")).default;

View File

@@ -82,6 +82,7 @@ import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
export default class DesktopLayout {
constructor(customWidgets) {
@@ -140,6 +141,7 @@ export default class DesktopLayout {
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new PromotedAttributesWidget())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())

View File

@@ -23,6 +23,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
const MOBILE_CSS = `
<style>
@@ -167,6 +168,7 @@ export default class MobileLayout {
.child(new NoteListWidget())
.child(new FilePropertiesWidget().css('font-size','smaller'))
)
.child(new ClassicEditorToolbar())
)
.child(new ProtectedSessionPasswordDialog())
.child(new ConfirmDialog())

View File

@@ -3,6 +3,7 @@ import keyboardActionService from '../services/keyboard_actions.js';
class ContextMenu {
constructor() {
this.$widget = $("#context-menu-container");
this.$widget.addClass("dropend");
this.dateContextMenuOpenedMs = 0;
$(document).on('click', () => this.hide());
@@ -11,6 +12,12 @@ class ContextMenu {
async show(options) {
this.options = options;
if (this.$widget.hasClass("show")) {
// The menu is already visible. Hide the menu then open it again
// at the new location to re-trigger the opening animation.
await this.hide();
}
this.$widget.empty();
this.addItems(this.$widget, options.items);
@@ -96,6 +103,10 @@ class ContextMenu {
.append(" &nbsp; ") // some space between icon and text
.append(item.title);
if (item.shortcut) {
$link.append($("<kbd>").text(item.shortcut));
}
const $item = $("<li>")
.addClass("dropdown-item")
.append($link)
@@ -142,18 +153,26 @@ class ContextMenu {
}
}
hide() {
async hide() {
// this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468
// "contextmenu" event also triggers "click" event which depending on the timing can close the just opened context menu
// we might filter out right clicks, but then it's better if even right clicks close the context menu
if (Date.now() - this.dateContextMenuOpenedMs > 300) {
// seems like if we hide the menu immediately, some clicks can get propagated to the underlying component
// see https://github.com/zadam/trilium/pull/3805 for details
setTimeout(() => this.$widget.hide(), 100);
await timeout(100);
this.$widget.removeClass("show");
this.$widget.hide()
}
}
}
function timeout(ms) {
return new Promise((accept, reject) => {
setTimeout(accept, ms);
});
}
const contextMenu = new ContextMenu();
export default contextMenu;

View File

@@ -2,6 +2,7 @@ import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu from "./context_menu.js";
import { t } from "../services/i18n.js";
function setupContextMenu() {
const electron = utils.dynamicRequire('electron');
@@ -28,7 +29,7 @@ function setupContextMenu() {
}
items.push({
title: `Add "${params.misspelledWord}" to dictionary`,
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
uiIcon: "bx bx-plus",
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
});
@@ -39,7 +40,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canCut && hasText,
title: `Cut <kbd>${platformModifier}+X`,
title: t("electron_context_menu.cut"),
shortcut: `${platformModifier}+X`,
uiIcon: "bx bx-cut",
handler: () => webContents.cut()
});
@@ -48,7 +50,8 @@ function setupContextMenu() {
if (params.isEditable || hasText) {
items.push({
enabled: editFlags.canCopy && hasText,
title: `Copy <kbd>${platformModifier}+C`,
title: t("electron_context_menu.copy"),
shortcut: `${platformModifier}+C`,
uiIcon: "bx bx-copy",
handler: () => webContents.copy()
});
@@ -56,7 +59,7 @@ function setupContextMenu() {
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
items.push({
title: `Copy link`,
title: t("electron_context_menu.copy-link"),
uiIcon: "bx bx-copy",
handler: () => {
electron.clipboard.write({
@@ -70,7 +73,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canPaste,
title: `Paste <kbd>${platformModifier}+V`,
title: t("electron_context_menu.paste"),
shortcut: `${platformModifier}+V`,
uiIcon: "bx bx-paste",
handler: () => webContents.paste()
});
@@ -79,7 +83,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canPaste,
title: `Paste as plain text <kbd>${platformModifier}+Shift+V`,
title: t("electron_context_menu.paste-as-plain-text"),
shortcut: `${platformModifier}+Shift+V`,
uiIcon: "bx bx-paste",
handler: () => webContents.pasteAndMatchStyle()
});
@@ -106,9 +111,11 @@ function setupContextMenu() {
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({title: "----"});
items.push({
enabled: editFlags.canPaste,
title: `Search for "${shortenedSelection}" with ${searchEngineName}`,
title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }),
uiIcon: "bx bx-search-alt",
handler: () => electron.shell.openExternal(searchUrl)
});

View File

@@ -20,9 +20,13 @@ function setupContextMenu($image) {
{
title: "Copy reference to clipboard",
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
uiIcon: "bx bx-directions"
},
{
title: "Copy image to clipboard",
command: "copyImageToClipboard",
uiIcon: "bx bx-copy"
},
{ title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty" },
],
selectMenuItemHandler: async ({ command }) => {
if (command === 'copyImageReferenceToClipboard') {

View File

@@ -3,6 +3,7 @@ import froca from "../services/froca.js";
import contextMenu from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from '../services/i18n.js';
export default class LauncherContextMenu {
/**
@@ -33,29 +34,31 @@ export default class LauncherContextMenu {
const isAvailableItem = parentNoteId === '_lbAvailableLaunchers';
const isItem = isVisibleItem || isAvailableItem;
const canBeDeleted = !note.noteId.startsWith("_"); // fixed notes can't be deleted
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();;
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
return [
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a note launcher', command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a script launcher', command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a custom widget', command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: 'Add spacer', command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
{ title: 'Reset', command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
(isVisibleItem || isAvailableItem) ? { title: "----" } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
{ title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
{ title: "----" },
isAvailableItem ? { title: 'Move to visible launchers', command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: 'Move to available launchers', command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
{ title: `Duplicate launcher <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: isItem }
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset}
].filter(row => row !== null);
}
async selectMenuItemHandler({command}) {
if (command === 'resetLauncher') {
const confirmed = await dialogService.confirm(`Do you really want to reset "${this.node.title}"?
All data / settings in this note (and its children) will be lost
and the launcher will be returned to its original location.`);
const confirmed = await dialogService.confirm(t("launcher_context_menu.reset_launcher_confirm", { title: this.node.title }));
if (confirmed) {
await server.post(`special-notes/launchers/${this.node.data.noteId}/reset`);

View File

@@ -6,7 +6,7 @@ function openContextMenu(notePath, e, viewScope = {}, hoistedNoteId = null) {
x: e.pageX,
y: e.pageY,
items: [
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"},
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-link-external"},
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
],

View File

@@ -49,56 +49,98 @@ export default class TreeContextMenu {
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.insert-note-after")} <kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bxs-chevrons-up", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: "----" },
{ title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.insert-child-note")} <kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
{ title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: "----" },
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bx-empty", enabled: true, items: [
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted && notOptions },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
] },
{ title: "----" },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: "----" },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted },
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
{ title: "----" },
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-rename", enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{ title: "----" },
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-sort-down", enabled: noSelectedNotes && notSearch },
{ title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
] },
{ title: "----" },
{ title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-transfer",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: "----" },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-empty",
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-empty",
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
enabled: true }
{ title: "----" },
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
].filter(row => row !== null);
}
@@ -136,7 +178,7 @@ export default class TreeContextMenu {
this.treeWidget.triggerCommand("openNewNoteSplit", {ntxId, notePath});
}
else if (command === 'convertNoteToAttachment') {
if (!await dialogService.confirm(`Are you sure you want to convert note selected notes into attachments of their parent notes?`)) {
if (!await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm"))) {
return;
}
@@ -154,7 +196,7 @@ export default class TreeContextMenu {
}
}
toastService.showMessage(`${converted} notes have been converted to attachments.`);
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
}
else if (command === 'copyNotePathToClipboard') {
navigator.clipboard.writeText('#' + notePath);

View File

@@ -5,6 +5,7 @@ import froca from "./froca.js";
import hoistedNoteService from "./hoisted_note.js";
import ws from "./ws.js";
import appContext from "../components/app_context.js";
import { t } from './i18n.js';
async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
branchIdsToMove = filterRootNote(branchIdsToMove);
@@ -13,7 +14,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
const beforeBranch = froca.getBranch(beforeBranchId);
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -42,7 +43,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
];
if (forbiddenNoteIds.includes(afterNote.noteId)) {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -62,7 +63,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
const newParentBranch = froca.getBranch(newParentBranchId);
if (newParentBranch.noteId === '_lbRoot') {
toastService.showError('Cannot move notes here.');
toastService.showError(t("branches.cannot-move-notes-here"));
return;
}
@@ -192,7 +193,7 @@ function filterRootNote(branchIds) {
function makeToast(id, message) {
return {
id: id,
title: "Delete status",
title: t("branches.delete-status"),
message: message,
icon: "trash"
};
@@ -207,9 +208,9 @@ ws.subscribeToMessages(async message => {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, `Delete notes in progress: ${message.progressCount}`));
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Delete finished successfully.");
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
toast.closeAfter = 5000;
toastService.showPersistent(toast);
@@ -225,9 +226,9 @@ ws.subscribeToMessages(async message => {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message.taskId, `Undeleting notes in progress: ${message.progressCount}`));
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
toast.closeAfter = 5000;
toastService.showPersistent(toast);

View File

@@ -13,22 +13,23 @@ import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js";
const ACTION_GROUPS = [
{
title: 'Labels',
title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
},
{
title: 'Relations',
title: t("bulk_actions.relations"),
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
},
{
title: 'Notes',
title: t("bulk_actions.notes"),
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
},
{
title: 'Other',
title: t("bulk_actions.other"),
actions: [ExecuteScriptBulkAction]
}
];

View File

@@ -3,6 +3,7 @@ import server from "./server.js";
import toastService from "./toast.js";
import froca from "./froca.js";
import utils from "./utils.js";
import { t } from "./i18n.js";
async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) {
const bundle = await server.post(`script/bundle/${noteId}`, {
@@ -75,9 +76,23 @@ async function getWidgetBundlesByParent() {
try {
widget = await executeBundle(bundle);
widgetsByParent.add(widget);
}
catch (e) {
if (widget) {
widget._noteId = bundle.noteId;
widgetsByParent.add(widget);
}
} catch (e) {
const noteId = bundle.noteId;
const note = await froca.getNote(noteId);
toastService.showPersistent({
title: t("toast.bundle-error.title"),
icon: "alert",
message: t("toast.bundle-error.message", {
id: noteId,
title: note.title,
message: e.message
})
});
logError("Widget initialization failed: ", e);
continue;
}

View File

@@ -3,6 +3,7 @@ import toastService from "./toast.js";
import froca from "./froca.js";
import linkService from "./link.js";
import utils from "./utils.js";
import { t } from "./i18n.js";
let clipboardBranchIds = [];
let clipboardMode = null;
@@ -78,7 +79,7 @@ async function copy(branchIds) {
clipboard.writeHTML(links.join(', '));
}
toastService.showMessage("Note(s) have been copied into clipboard.");
toastService.showMessage(t("clipboard.copied"));
}
function cut(branchIds) {
@@ -87,7 +88,7 @@ function cut(branchIds) {
if (clipboardBranchIds.length > 0) {
clipboardMode = 'cut';
toastService.showMessage("Note(s) have been cut into clipboard.");
toastService.showMessage(t("clipboard.cut"));
}
}

View File

@@ -10,6 +10,8 @@ import treeService from "./tree.js";
import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
import mime_types from "./mime_types.js";
let idCounter = 1;
@@ -105,16 +107,25 @@ async function renderText(note, $renderedContent) {
for (const el of referenceLinks) {
await linkService.loadReferenceLinkTitle($(el));
}
await applySyntaxHighlight($renderedContent);
} else {
await renderChildrenList($renderedContent, note);
}
}
/** @param {FNote} note */
/**
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
*
* @param {FNote} note
*/
async function renderCode(note, $renderedContent) {
const blob = await note.getBlob();
$renderedContent.append($("<pre>").text(blob.content));
const $codeBlock = $("<code>");
$codeBlock.text(blob.content);
$renderedContent.append($("<pre>").append($codeBlock));
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime));
}
function renderImage(entity, $renderedContent, options = {}) {

View File

@@ -217,8 +217,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/
this.runOnBackend = async (func, params = []) => {
if (func?.constructor.name === "AsyncFunction" || func?.startsWith?.("async ")) {
toastService.showError("You're passing an async function to api.runOnBackend() which will likely not work as you intended. "
+ "Either make the function synchronous (by removing 'async' keyword), or use api.runAsyncOnBackendWithManualTransactionHandling()");
toastService.showError(t("frontend_script_api.async_warning"));
}
return await this.__runOnBackendInner(func, params, true);
@@ -240,8 +239,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
*/
this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => {
if (func?.constructor.name === "Function" || func?.startsWith?.("function")) {
toastService.showError("You're passing a synchronous function to api.runAsyncOnBackendWithManualTransactionHandling(), " +
"while you should likely use api.runOnBackend() instead.");
toastService.showError(t("frontend_script_api.sync_warning"));
}
return await this.__runOnBackendInner(func, params, false);

View File

@@ -26,21 +26,6 @@ function setupGlobs() {
// for CKEditor integration (button on block toolbar)
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
window.glob.SEARCH_HELP_TEXT = `
<strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="search.html">complete help on search</button>
<p>
<ul>
<li>Just enter any text for full text search</li>
<li><code>#abc</code> - returns notes with label abc</li>
<li><code>#year = 2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li>
<li><code>#rock #pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li>
<li><code>#rock or #pop</code> - only one of the labels must be present</li>
<li><code>#year &lt;= 2000</code> - numerical comparison (also &gt;, &gt;=, &lt;).</li>
<li><code>note.dateCreated >= MONTH-1</code> - notes created in the last month</li>
<li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li>
</ul>
</p>`;
window.onerror = function (msg, url, lineNo, columnNo, error) {
const string = msg.toLowerCase();
@@ -64,6 +49,28 @@ function setupGlobs() {
return false;
};
window.addEventListener("unhandledrejection", (e) => {
const string = e?.reason?.message?.toLowerCase();
let message = "Uncaught error: ";
if (string?.includes("script error")) {
message += 'No details available';
} else {
message += [
`Message: ${e.reason.message}`,
`Line: ${e.reason.lineNumber}`,
`Column: ${e.reason.columnNumber}`,
`Error object: ${JSON.stringify(e.reason)}`,
`Stack: ${e.reason && e.reason.stack}`
].join(', ');
}
ws.logError(message);
return false;
});
for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false);
}

View File

@@ -2,6 +2,7 @@ import appContext from "../components/app_context.js";
import treeService from "./tree.js";
import dialogService from "./dialog.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
function getHoistedNoteId() {
const activeNoteContext = appContext.tabManager.getActiveContext();
@@ -53,7 +54,7 @@ async function checkNoteAccess(notePath, noteContext) {
const hoistedNote = await froca.getNote(hoistedNoteId);
if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
&& !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote.title, hoistedNote: hoistedNote.title }))) {
return false;
}

View File

@@ -8,9 +8,9 @@ function copyImageReferenceToClipboard($imageWrapper) {
const success = document.execCommand('copy');
if (success) {
toastService.showMessage("A reference to the image has been copied to clipboard. This can be pasted in any text note.");
toastService.showMessage(t("image.copied-to-clipboard"));
} else {
toastService.showAndLogError("Could not copy the image reference to clipboard.");
toastService.showAndLogError(t("image.cannot-copy"));
}
}
finally {

View File

@@ -36,7 +36,7 @@ export async function uploadFiles(entityType, parentNoteId, files, options) {
type: 'POST',
timeout: 60 * 60 * 1000,
error: function (xhr) {
toastService.showError(`Import failed: ${xhr.responseText}`);
toastService.showError(t("import.failed", { message: xhr.responseText }));
},
contentType: false, // NEEDED, DON'T REMOVE THIS
processData: false, // NEEDED, DON'T REMOVE THIS

View File

@@ -1,3 +1,7 @@
import mimeTypesService from "./mime_types.js";
import optionsService from "./options.js";
import { getStylesheetUrl } from "./syntax_highlight.js";
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
const CODE_MIRROR = {
@@ -84,18 +88,44 @@ const MIND_ELIXIR = {
]
};
const HIGHLIGHT_JS = {
js: () => {
const mimeTypes = mimeTypesService.getMimeTypes();
const scriptsToLoad = new Set();
scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js");
for (const mimeType of mimeTypes) {
if (mimeType.enabled && mimeType.highlightJs) {
scriptsToLoad.add(`node_modules/@highlightjs/cdn-assets/languages/${mimeType.highlightJs}.min.js`);
}
}
const currentTheme = optionsService.get("codeBlockTheme");
loadHighlightingTheme(currentTheme);
return Array.from(scriptsToLoad);
}
};
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
for (const scriptUrl of unwrapValue(library.js)) {
await requireScript(scriptUrl);
}
}
}
function unwrapValue(value) {
if (typeof value === "function") {
return value();
}
return value;
}
// we save the promises in case of the same script being required concurrently multiple times
const loadedScriptPromises = {};
@@ -127,9 +157,36 @@ async function requireCss(url, prependAssetPath = true) {
}
}
let highlightingThemeEl = null;
function loadHighlightingTheme(theme) {
if (!theme) {
return;
}
if (theme === "none") {
// Deactivate the theme.
if (highlightingThemeEl) {
highlightingThemeEl.remove();
highlightingThemeEl = null;
}
return;
}
if (!highlightingThemeEl) {
highlightingThemeEl = $(`<link rel="stylesheet" type="text/css" />`);
$("head").append(highlightingThemeEl);
}
const url = getStylesheetUrl(theme);
if (url) {
highlightingThemeEl.attr("href", url);
}
}
export default {
requireCss,
requireLibrary,
loadHighlightingTheme,
CKEDITOR,
CODE_MIRROR,
ESLINT,
@@ -143,5 +200,6 @@ export default {
EXCALIDRAW,
MARKJS,
I18NEXT,
MIND_ELIXIR
MIND_ELIXIR,
HIGHLIGHT_JS
}

View File

@@ -254,8 +254,15 @@ function goToLinkExt(evt, hrefLink, $link) {
window.open(hrefLink, '_blank');
} else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
const electron = utils.dynamicRequire('electron');
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
// Adding `:` to these links might be safer.
const otherAllowedProtocols = ['mailto:', 'tel:', 'sms:', 'sftp:', 'smb:', 'slack:', 'zotero:'];
if (otherAllowedProtocols.some(protocol => hrefLink.toLowerCase().startsWith(protocol))){
window.open(hrefLink, '_blank');
}
}
}
}

View File

@@ -1,162 +1,171 @@
import options from "./options.js";
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
const MIME_TYPE_AUTO = "text-x-trilium-auto";
/**
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
*/
const MIME_TYPES_DICT = [
{ default: true, title: "Plain text", mime: "text/plain" },
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
{ title: "APL", mime: "text/apl" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
{ title: "ASP.NET", mime: "application/x-aspx" },
{ title: "Asterisk", mime: "text/x-asterisk" },
{ title: "Brainfuck", mime: "text/x-brainfuck" },
{ default: true, title: "C", mime: "text/x-csrc" },
{ default: true, title: "C#", mime: "text/x-csharp" },
{ default: true, title: "C++", mime: "text/x-c++src" },
{ title: "Clojure", mime: "text/x-clojure" },
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
{ default: true, title: "C", mime: "text/x-csrc", highlightJs: "c" },
{ default: true, title: "C#", mime: "text/x-csharp", highlightJs: "csharp" },
{ default: true, title: "C++", mime: "text/x-c++src", highlightJs: "cpp" },
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
{ title: "ClojureScript", mime: "text/x-clojurescript" },
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
{ title: "CMake", mime: "text/x-cmake" },
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
{ title: "Cobol", mime: "text/x-cobol" },
{ title: "CoffeeScript", mime: "text/coffeescript" },
{ title: "Common Lisp", mime: "text/x-common-lisp" },
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
{ title: "CQL", mime: "text/x-cassandra" },
{ title: "Crystal", mime: "text/x-crystal" },
{ default: true, title: "CSS", mime: "text/css" },
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
{ default: true, title: "CSS", mime: "text/css", highlightJs: "css" },
{ title: "Cypher", mime: "application/x-cypher-query" },
{ title: "Cython", mime: "text/x-cython" },
{ title: "D", mime: "text/x-d" },
{ title: "Dart", mime: "application/dart" },
{ title: "diff", mime: "text/x-diff" },
{ title: "Django", mime: "text/x-django" },
{ title: "Dockerfile", mime: "text/x-dockerfile" },
{ title: "D", mime: "text/x-d", highlightJs: "d" },
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
{ title: "DTD", mime: "application/xml-dtd" },
{ title: "Dylan", mime: "text/x-dylan" },
{ title: "EBNF", mime: "text/x-ebnf" },
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
{ title: "ECL", mime: "text/x-ecl" },
{ title: "edn", mime: "application/edn" },
{ title: "Eiffel", mime: "text/x-eiffel" },
{ title: "Elm", mime: "text/x-elm" },
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
{ title: "Embedded Javascript", mime: "application/x-ejs" },
{ title: "Embedded Ruby", mime: "application/x-erb" },
{ title: "Erlang", mime: "text/x-erlang" },
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
{ title: "Esper", mime: "text/x-esper" },
{ title: "F#", mime: "text/x-fsharp" },
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
{ title: "Factor", mime: "text/x-factor" },
{ title: "FCL", mime: "text/x-fcl" },
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "Gherkin", mime: "text/x-feature" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm" },
{ default: true, title: "Go", mime: "text/x-go" },
{ default: true, title: "Groovy", mime: "text/x-groovy" },
{ title: "HAML", mime: "text/x-haml" },
{ default: true, title: "Haskell", mime: "text/x-haskell" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ default: true, title: "Go", mime: "text/x-go", highlightJs: "go" },
{ default: true, title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy" },
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
{ default: true, title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell" },
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
{ title: "Haxe", mime: "text/x-haxe" },
{ default: true, title: "HTML", mime: "text/html" },
{ default: true, title: "HTTP", mime: "message/http" },
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
{ default: true, title: "HTML", mime: "text/html", highlightJs: "xml" },
{ default: true, title: "HTTP", mime: "message/http", highlightJs: "http" },
{ title: "HXML", mime: "text/x-hxml" },
{ title: "IDL", mime: "text/x-idl" },
{ default: true, title: "Java", mime: "text/x-java" },
{ title: "Java Server Pages", mime: "application/x-jsp" },
{ default: true, title: "Java", mime: "text/x-java", highlightJs: "java" },
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
{ title: "Jinja2", mime: "text/jinja2" },
{ default: true, title: "JS backend", mime: "application/javascript;env=backend" },
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend" },
{ default: true, title: "JSON", mime: "application/json" },
{ title: "JSON-LD", mime: "application/ld+json" },
{ title: "JSX", mime: "text/jsx" },
{ title: "Julia", mime: "text/x-julia" },
{ default: true, title: "Kotlin", mime: "text/x-kotlin" },
{ title: "LaTeX", mime: "text/x-latex" },
{ title: "LESS", mime: "text/x-less" },
{ title: "LiveScript", mime: "text/x-livescript" },
{ title: "Lua", mime: "text/x-lua" },
{ title: "MariaDB SQL", mime: "text/x-mariadb" },
{ default: true, title: "Markdown", mime: "text/x-markdown" },
{ title: "Mathematica", mime: "text/x-mathematica" },
{ default: true, title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript" },
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript" },
{ default: true, title: "JSON", mime: "application/json", highlightJs: "json" },
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
{ default: true, title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin" },
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
{ default: true, title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown" },
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
{ title: "mbox", mime: "application/mbox" },
{ title: "mIRC", mime: "text/mirc" },
{ title: "Modelica", mime: "text/x-modelica" },
{ title: "MS SQL", mime: "text/x-mssql" },
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
{ title: "mscgen", mime: "text/x-mscgen" },
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql" },
{ title: "Nginx", mime: "text/x-nginx-conf" },
{ title: "NSIS", mime: "text/x-nsis" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },
{ title: "Objective-C", mime: "text/x-objectivec" },
{ title: "OCaml", mime: "text/x-ocaml" },
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
{ title: "Octave", mime: "text/x-octave" },
{ title: "Oz", mime: "text/x-oz" },
{ title: "Pascal", mime: "text/x-pascal" },
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
{ title: "PEG.js", mime: "null" },
{ default: true, title: "Perl", mime: "text/x-perl" },
{ title: "PGP", mime: "application/pgp" },
{ default: true, title: "PHP", mime: "text/x-php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "PLSQL", mime: "text/x-plsql" },
{ title: "PostgreSQL", mime: "text/x-pgsql" },
{ title: "PowerShell", mime: "application/x-powershell" },
{ title: "Properties files", mime: "text/x-properties" },
{ title: "ProtoBuf", mime: "text/x-protobuf" },
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
{ title: "Pug", mime: "text/x-pug" },
{ title: "Puppet", mime: "text/x-puppet" },
{ default: true, title: "Python", mime: "text/x-python" },
{ title: "Q", mime: "text/x-q" },
{ title: "R", mime: "text/x-rsrc" },
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
{ default: true, title: "Python", mime: "text/x-python", highlightJs: "python" },
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
{ title: "reStructuredText", mime: "text/x-rst" },
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
{ default: true, title: "Ruby", mime: "text/x-ruby" },
{ title: "Rust", mime: "text/x-rustsrc" },
{ title: "SAS", mime: "text/x-sas" },
{ default: true, title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby" },
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
{ title: "Sass", mime: "text/x-sass" },
{ title: "Scala", mime: "text/x-scala" },
{ title: "Scheme", mime: "text/x-scheme" },
{ title: "SCSS", mime: "text/x-scss" },
{ default: true, title: "Shell (bash)", mime: "text/x-sh" },
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash" },
{ title: "Sieve", mime: "application/sieve" },
{ title: "Slim", mime: "text/x-slim" },
{ title: "Smalltalk", mime: "text/x-stsrc" },
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
{ title: "Smarty", mime: "text/x-smarty" },
{ title: "SML", mime: "text/x-sml" },
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
{ title: "Solr", mime: "text/x-solr" },
{ title: "Soy", mime: "text/x-soy" },
{ title: "SPARQL", mime: "application/sparql-query" },
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
{ default: true, title: "SQL", mime: "text/x-sql" },
{ title: "SQLite", mime: "text/x-sqlite" },
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium" },
{ default: true, title: "SQL", mime: "text/x-sql", highlightJs: "sql" },
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql" },
{ title: "Squirrel", mime: "text/x-squirrel" },
{ title: "sTeX", mime: "text/x-stex" },
{ title: "Stylus", mime: "text/x-styl" },
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
{ default: true, title: "Swift", mime: "text/x-swift" },
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
{ title: "Tcl", mime: "text/x-tcl" },
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
{ title: "Textile", mime: "text/x-textile" },
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
{ title: "Tiki wiki", mime: "text/tiki" },
{ title: "TOML", mime: "text/x-toml" },
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
{ title: "Tornado", mime: "text/x-tornado" },
{ title: "troff", mime: "text/troff" },
{ title: "TTCN", mime: "text/x-ttcn" },
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
{ title: "Turtle", mime: "text/turtle" },
{ title: "Twig", mime: "text/x-twig" },
{ title: "TypeScript", mime: "application/typescript" },
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
{ title: "TypeScript-JSX", mime: "text/typescript-jsx" },
{ title: "VB.NET", mime: "text/x-vb" },
{ title: "VBScript", mime: "text/vbscript" },
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
{ title: "Velocity", mime: "text/velocity" },
{ title: "Verilog", mime: "text/x-verilog" },
{ title: "VHDL", mime: "text/x-vhdl" },
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
{ title: "Vue.js Component", mime: "text/x-vue" },
{ title: "Web IDL", mime: "text/x-webidl" },
{ default: true, title: "XML", mime: "text/xml" },
{ title: "XQuery", mime: "application/xquery" },
{ default: true, title: "XML", mime: "text/xml", highlightJs: "xml" },
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
{ title: "xu", mime: "text/x-xu" },
{ title: "Yacas", mime: "text/x-yacas" },
{ default: true, title: "YAML", mime: "text/x-yaml" },
{ default: true, title: "YAML", mime: "text/x-yaml", highlightJs: "yaml" },
{ title: "Z80", mime: "text/x-z80" }
];
@@ -173,7 +182,7 @@ function loadMimeTypes() {
}
}
async function getMimeTypes() {
function getMimeTypes() {
if (mimeTypes === null) {
loadMimeTypes();
}
@@ -181,7 +190,46 @@ async function getMimeTypes() {
return mimeTypes;
}
export default {
getMimeTypes,
loadMimeTypes
let mimeToHighlightJsMapping = null;
/**
* Obtains the corresponding language tag for highlight.js for a given MIME type.
*
* The mapping is built the first time this method is built and then the results are cached for better performance.
*
* @param {string} mimeType The MIME type of the code block, in the CKEditor-normalized format (e.g. `text-c-src` instead of `text/c-src`).
* @returns the corresponding highlight.js tag, for example `c` for `text-c-src`.
*/
function getHighlightJsNameForMime(mimeType) {
if (!mimeToHighlightJsMapping) {
const mimeTypes = getMimeTypes();
mimeToHighlightJsMapping = {};
for (const mimeType of mimeTypes) {
// The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup.
const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs;
}
}
return mimeToHighlightJsMapping[mimeType];
}
/**
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
* code plugin.
*
* @param {string} mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
* @returns the normalized MIME type (e.g. `text-c-src`).
*/
function normalizeMimeTypeForCKEditor(mimeType) {
return mimeType.toLowerCase()
.replace(/[\W_]+/g,"-");
}
export default {
MIME_TYPE_AUTO,
getMimeTypes,
loadMimeTypes,
getHighlightJsNameForMime,
normalizeMimeTypeForCKEditor
}

View File

@@ -45,6 +45,16 @@ async function autocompleteSource(term, cb, options = {}) {
].concat(results);
}
if (term.trim().length >= 1 && options.allowSearchNotes) {
results = results.concat([
{
action: 'search-notes',
noteTitle: term,
highlightedNotePathTitle: `Search for "${utils.escapeHtml(term)}" <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
}
]);
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
@@ -138,6 +148,17 @@ function initNoteAutocomplete($el, options) {
autocompleteOptions.debug = true; // don't close on blur
}
if (options.allowSearchNotes) {
$el.on('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
// Prevent Ctrl + Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
$el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
}
});
}
$el.autocomplete({
...autocompleteOptions,
appendTo: document.querySelector('body'),
@@ -192,6 +213,12 @@ function initNoteAutocomplete($el, options) {
suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
}
if (suggestion.action === 'search-notes') {
const searchString = suggestion.noteTitle;
appContext.triggerCommand('searchNotes', { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);

View File

@@ -5,6 +5,7 @@ import ws from "./ws.js";
import froca from "./froca.js";
import treeService from "./tree.js";
import toastService from "./toast.js";
import { t } from "./i18n.js";
async function createNote(parentNotePath, options = {}) {
options = Object.assign({
@@ -119,7 +120,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
const origNote = await froca.getNote(noteId);
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
toastService.showMessage(t("note_create.duplicated", { title: origNote.title }));
}
export default {

View File

@@ -366,12 +366,13 @@ class NoteListRenderer {
separateWordSearch: false,
caseSensitive: false
});
}
}
$content.append($renderedContent);
$content.addClass(`type-${type}`);
} catch (e) {
console.log(`Caught error while rendering note '${note.noteId}' of type '${note.type}': ${e.message}, stack: ${e.stack}`);
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
console.error(e);
$content.append("rendering error");
}

View File

@@ -6,6 +6,7 @@ import appContext from "../components/app_context.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import { t } from './i18n.js';
let protectedSessionDeferred = null;
@@ -50,7 +51,7 @@ async function setupProtectedSession(password) {
const response = await server.post('login/protected', { password: password });
if (!response.success) {
toastService.showError("Wrong password.", 3000);
toastService.showError(t("protected_session.wrong_password"), 3000);
return;
}
@@ -72,7 +73,7 @@ ws.subscribeToMessages(async message => {
protectedSessionDeferred = null;
}
toastService.showMessage("Protected session has been started.");
toastService.showMessage(t("protected_session.started"));
}
else if (message.type === 'protectedSessionLogout') {
utils.reloadFrontendApp(`Protected session logout`);
@@ -85,10 +86,10 @@ async function protectNote(noteId, protect, includingSubtree) {
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
}
function makeToast(message, protectingLabel, text) {
function makeToast(message, title, text) {
return {
id: message.taskId,
title: `${protectingLabel} status`,
title,
message: text,
icon: message.data.protect ? "check-shield" : "shield"
};
@@ -99,15 +100,19 @@ ws.subscribeToMessages(async message => {
return;
}
const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting";
const isProtecting = message.data.protect;
const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title");
if (message.type === 'taskError') {
toastService.closePersistent(message.taskId);
toastService.showError(message.message);
} else if (message.type === 'taskProgressCount') {
toastService.showPersistent(makeToast(message, protectingLabel,`${protectingLabel} in progress: ${message.progressCount}`));
const count = message.progressCount;
const text = ( isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count }));
toastService.showPersistent(makeToast(message, title, text));
} else if (message.type === 'taskSucceeded') {
const toast = makeToast(message, protectingLabel, `${protectingLabel} finished successfully.`);
const text = (isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully"))
const toast = makeToast(message, title, text);
toast.closeAfter = 3000;
toastService.showPersistent(toast);

View File

@@ -1,3 +1,4 @@
import { t } from './i18n.js';
import server from './server.js';
import toastService from "./toast.js";
@@ -5,7 +6,7 @@ async function syncNow(ignoreNotConfigured = false) {
const result = await server.post('sync/now');
if (result.success) {
toastService.showMessage("Sync finished successfully.");
toastService.showMessage(t("sync.finished-successfully"));
}
else {
if (result.message.length > 200) {
@@ -13,7 +14,7 @@ async function syncNow(ignoreNotConfigured = false) {
}
if (!ignoreNotConfigured || result.errorCode !== 'NOT_CONFIGURED') {
toastService.showError(`Sync failed: ${result.message}`);
toastService.showError(t("sync.failed", { message: result.message }));
}
}
}

View File

@@ -0,0 +1,94 @@
import library_loader from "./library_loader.js";
import mime_types from "./mime_types.js";
import options from "./options.js";
export function getStylesheetUrl(theme) {
if (!theme) {
return null;
}
const defaultPrefix = "default:";
if (theme.startsWith(defaultPrefix)) {
return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`;
}
return null;
}
/**
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
*
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
*/
export async function applySyntaxHighlight($container) {
if (!isSyntaxHighlightEnabled()) {
return;
}
const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) {
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
if (!normalizedMimeType) {
continue;
}
applySingleBlockSyntaxHighlight($(codeBlock, normalizedMimeType));
}
}
/**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*
* @param {*} $codeBlock
* @param {*} normalizedMimeType
*/
export async function applySingleBlockSyntaxHighlight($codeBlock, normalizedMimeType) {
$codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text();
if (!window.hljs) {
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
}
let highlightedText = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
highlightedText = hljs.highlightAuto(text);
} else if (normalizedMimeType) {
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType);
if (language) {
highlightedText = hljs.highlight(text, { language });
} else {
console.warn(`Unknown mime type: ${normalizedMimeType}.`);
}
}
if (highlightedText) {
$codeBlock.html(highlightedText.value);
}
}
/**
* Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option.
* @returns whether syntax highlighting should be enabled for code blocks.
*/
export function isSyntaxHighlightEnabled() {
const theme = options.get("codeBlockTheme");
return theme && theme !== "none";
}
/**
* Given a HTML element, tries to extract the `language-` class name out of it.
*
* @param {string} el the HTML element from which to extract the language tag.
* @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
*/
function extractLanguageFromClassList(el) {
const prefix = "language-";
for (const className of el.classList) {
if (className.startsWith(prefix)) {
return className.substr(prefix.length);
}
}
return null;
}

View File

@@ -16,7 +16,7 @@ function toast(options) {
);
$toast.find('.toast-title').text(options.title);
$toast.find('.toast-body').text(options.message);
$toast.find('.toast-body').html(options.message);
if (options.id) {
$toast.attr("id", `toast-${options.id}`);

View File

@@ -527,6 +527,58 @@ function downloadSvg(nameWithoutExtension, svgContent) {
document.body.removeChild(element);
}
/**
* Compares two semantic version strings.
* Returns:
* 1 if v1 is greater than v2
* 0 if v1 is equal to v2
* -1 if v1 is less than v2
*
* @param {string} v1 First version string
* @param {string} v2 Second version string
* @returns {number}
*/
function compareVersions(v1, v2) {
// Remove 'v' prefix and everything after dash if present
v1 = v1.replace(/^v/, '').split('-')[0];
v2 = v2.replace(/^v/, '').split('-')[0];
const v1parts = v1.split('.').map(Number);
const v2parts = v2.split('.').map(Number);
// Pad shorter version with zeros
while (v1parts.length < 3) v1parts.push(0);
while (v2parts.length < 3) v2parts.push(0);
// Compare major version
if (v1parts[0] !== v2parts[0]) {
return v1parts[0] > v2parts[0] ? 1 : -1;
}
// Compare minor version
if (v1parts[1] !== v2parts[1]) {
return v1parts[1] > v2parts[1] ? 1 : -1;
}
// Compare patch version
if (v1parts[2] !== v2parts[2]) {
return v1parts[2] > v2parts[2] ? 1 : -1;
}
return 0;
}
/**
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
* @param {string} latestVersion
* @param {string} currentVersion
* @returns {boolean}
*/
function isUpdateAvailable(latestVersion, currentVersion) {
return compareVersions(latestVersion, currentVersion) > 0;
}
export default {
reloadFrontendApp,
parseDate,
@@ -567,5 +619,7 @@ export default {
areObjectsEqual,
copyHtmlToClipboard,
createImageSrcUrl,
downloadSvg
downloadSvg,
compareVersions,
isUpdateAvailable
};

View File

@@ -114,10 +114,10 @@ async function handleMessage(event) {
await executeFrontendUpdate(message.data.entityChanges);
}
else if (message.type === 'sync-hash-check-failed') {
toastService.showError("Sync check failed!", 60000);
toastService.showError(t("ws.sync-check-failed"), 60000);
}
else if (message.type === 'consistency-checks-failed') {
toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
}
else if (message.type === 'api-log-messages') {
appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
@@ -189,7 +189,7 @@ async function consumeFrontendUpdateData() {
else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toastService.showError(`Encountered error "${e.message}", check out the console.`);
toastService.showError(t("ws.encountered-error", { message: e.message }));
}
}

View File

@@ -347,8 +347,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
this.$editor.on("click", e => this.handleEditorClick(e));
/** @property {BalloonEditor} */
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig);
this.textEditor.model.document.on('change:data', () => this.dataChanged());
this.textEditor.editing.view.document.on('enter', (event, data) => {
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
@@ -358,9 +357,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
// disable spellcheck for attribute editor
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
//await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector');
//CKEditorInspector.attach(this.textEditor);
}
dataChanged() {

View File

@@ -1,4 +1,5 @@
import Component from "../components/component.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import toastService from "../services/toast.js";
@@ -84,15 +85,8 @@ class BasicWidget extends Component {
render() {
try {
this.doRender();
} catch (e) {
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message", {
title: this.widgetTitle,
message: e.message
})
});
} catch (e) {
this.logRenderingError(e);
}
this.$widget.attr('data-component-id', this.componentId);
@@ -131,6 +125,35 @@ class BasicWidget extends Component {
return this.$widget;
}
logRenderingError(e) {
console.log("Got issue in widget ", this);
console.error(e);
let noteId = this._noteId;
if (this._noteId) {
froca.getNote(noteId, true).then((note) => {
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message-custom", {
id: noteId,
title: note.title,
message: e.message
})
});
});
return;
}
toastService.showPersistent({
title: t("toast.widget-error.title"),
icon: "alert",
message: t("toast.widget-error.message-unknown", {
message: e.message
})
});
}
/**
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
* @returns

View File

@@ -20,6 +20,13 @@ const TPL = `
width: 20em;
}
.attachment-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
}
.attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
@@ -32,16 +39,39 @@ const TPL = `
style="position: relative; top: 3px;"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="openAttachment" class="dropdown-item"
title="${t('attachments_actions.open_externally_title')}">${t('attachments_actions.open_externally')}</a>
<a data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t('attachments_actions.open_custom_title')}">${t('attachments_actions.open_custom')}</a>
<a data-trigger-command="downloadAttachment" class="dropdown-item">${t('attachments_actions.download')}</a>
<a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a>
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">${t('attachments_actions.upload_new_revision')}</a>
<a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">${t('attachments_actions.copy_link_to_clipboard')}</a>
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a>
<a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
<li data-trigger-command="openAttachment" class="dropdown-item"
title="${t('attachments_actions.open_externally_title')}"><span class="bx bx-file-find"></span> ${t('attachments_actions.open_externally')}</li>
<li data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t('attachments_actions.open_custom_title')}"><span class="bx bx-customize"></span> ${t('attachments_actions.open_custom')}</li>
<li data-trigger-command="downloadAttachment" class="dropdown-item">
<span class="bx bx-download"></span> ${t('attachments_actions.download')}</li>
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
</span> ${t('attachments_actions.copy_link_to_clipboard')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
</span> ${t('attachments_actions.upload_new_revision')}</li>
<li data-trigger-command="renameAttachment" class="dropdown-item">
<span class="bx bx-rename"></span> ${t('attachments_actions.rename_attachment')}</li>
<li data-trigger-command="deleteAttachment" class="dropdown-item">
<span class="bx bx-trash destructive-action-icon"></span> ${t('attachments_actions.delete_attachment')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
</span> ${t('attachments_actions.convert_attachment_into_note')}</li>
</div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
@@ -83,14 +113,14 @@ export default class AttachmentActionsWidget extends BasicWidget {
const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
$openAttachmentButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
if (isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
}
@@ -99,7 +129,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_custom_client_only'))
);
}

View File

@@ -100,6 +100,8 @@ const TPL = `
position: relative;
left: 0;
top: 5px;
--dropdown-shadow-opacity: 0;
--submenu-opening-delay: 0;
}
</style>
@@ -125,28 +127,20 @@ const TPL = `
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li class="dropdown-item" data-trigger-command="showOptions">
<span class="bx bx-cog"></span>
${t('global_menu.options')}
</li>
<li class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span>
${t('global_menu.open_new_window')}
<kbd data-command="openNewWindow"></kbd>
</li>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
<span class="bx bx-mobile"></span>
${t('global_menu.switch_to_mobile_version')}
<li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-share-alt"></span>
${t('global_menu.show_shared_notes_subtree')}
</li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
<span class="bx bx-desktop"></span>
${t('global_menu.switch_to_desktop_version')}
</li>
<span class="zoom-container dropdown-item">
<div class="dropdown-divider"></div>
<span class="zoom-container dropdown-item dropdown-item-container">
<div>
<span class="bx bx-empty"></span>
${t('global_menu.zoom')}
@@ -165,16 +159,23 @@ const TPL = `
</div>
</span>
<div class="dropdown-divider zoom-container-separator"></div>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
<span class="bx bx-mobile"></span>
${t('global_menu.switch_to_mobile_version')}
</li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
<span class="bx bx-desktop"></span>
${t('global_menu.switch_to_desktop_version')}
</li>
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
<span class="bx bx-sidebar"></span>
${t('global_menu.configure_launchbar')}
</li>
<li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-share-alt"></span>
${t('global_menu.show_shared_notes_subtree')}
</li>
<li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle">
<span class="bx bx-chip"></span>
@@ -182,10 +183,22 @@ const TPL = `
</span>
<ul class="dropdown-menu">
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-bug-alt"></span>
${t('global_menu.open_dev_tools')}
<kbd data-command="openDevTools"></kbd>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
<li class="dropdown-item" data-trigger-command="showSearchHistory">
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')}
</li>
<div class="dropdown-divider"></div>
<li class="dropdown-item" data-trigger-command="showBackendLog">
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showSQLConsole">
@@ -198,16 +211,13 @@ const TPL = `
<span class="bx bx-data"></span>
${t('global_menu.open_sql_console_history')}
</li>
<li class="dropdown-item" data-trigger-command="showSearchHistory">
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')}
</li>
<li class="dropdown-item" data-trigger-command="showBackendLog">
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
<div class="dropdown-divider"></div>
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-bug-alt"></span>
${t('global_menu.open_dev_tools')}
<kbd data-command="openDevTools"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
@@ -217,13 +227,16 @@ const TPL = `
<kbd data-command="reloadFrontendApp"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
</ul>
</li>
<li class="dropdown-item" data-trigger-command="showOptions">
<span class="bx bx-cog"></span>
${t('global_menu.options')}
</li>
<div class="dropdown-divider"></div>
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-help-circle"></span>
${t('global_menu.show_help')}
@@ -241,6 +254,8 @@ const TPL = `
<span class="version-text"></span>
</li>
<div class="dropdown-divider logout-button-separator"></div>
<li class="dropdown-item logout-button" data-trigger-command="logout">
<span class="bx bx-log-out"></span>
${t('global_menu.logout')}
@@ -268,6 +283,8 @@ export default class GlobalMenuWidget extends BasicWidget {
const isElectron = utils.isElectron();
this.$widget.find(".logout-button").toggle(!isElectron);
this.$widget.find(".logout-button-separator").toggle(!isElectron);
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
@@ -293,6 +310,7 @@ export default class GlobalMenuWidget extends BasicWidget {
if (!utils.isElectron()) {
this.$widget.find(".zoom-container").hide();
this.$widget.find(".zoom-container-separator").hide();
}
this.$zoomState = this.$widget.find(".zoom-state");
@@ -333,7 +351,8 @@ export default class GlobalMenuWidget extends BasicWidget {
const latestVersion = await this.fetchLatestVersion();
this.updateAvailableWidget.updateVersionStatus(latestVersion);
this.$updateToLatestVersionButton.toggle(latestVersion > glob.triliumVersion);
// Show "click to download" button in options menu if there's a new version available
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
}

View File

@@ -11,42 +11,91 @@ import { t } from "../../services/i18n.js";
const TPL = `
<div class="dropdown note-actions">
<style>
.note-actions {
width: 35px;
height: 35px;
}
.note-actions .dropdown-menu {
min-width: 15em;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
.note-actions {
width: 35px;
height: 35px;
}
.note-actions .dropdown-menu {
min-width: 15em;
}
.note-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a>
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a>
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> ${t('note_actions.note_source')}</a>
<a data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"><kbd data-command="showAttachments"></kbd> ${t('note_actions.note_attachments')}</a>
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
title="${t('note_actions.open_note_externally_title')}">
<kbd data-command="openNoteExternally"></kbd>
${t('note_actions.open_note_externally')}
</a>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a>
<a class="dropdown-item import-files-button">${t('note_actions.import_files')}</a>
<a class="dropdown-item export-note-button">${t('note_actions.export_note')}</a>
<a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> ${t('note_actions.print_note')}</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> ${t('note_actions.save_revision')}</a>
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<span class="bx bx-paperclip"></span> ${t('note_actions.convert_into_attachment')}
</li>
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<span class="bx bx-extension"></span> ${t('note_actions.re_render_note')}<kbd data-command="renderActiveNote"></kbd>
</li>
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
<span class='bx bx-search'></span> ${t('note_actions.search_in_note')}<kbd data-command="findInText"></kbd>
</li>
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t('note_actions.print_note')}<kbd data-command="printActiveNote"></kbd></li>
<div class="dropdown-divider"></div>
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t('note_actions.import_files')}</li>
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t('note_actions.export_note')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t('note_actions.open_note_externally_title')}">
<span class="bx bx-file-find"></span> ${t('note_actions.open_note_externally')}<kbd data-command="openNoteExternally"></kbd>
</li>
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
<span class="bx bx-customize"></span> ${t('note_actions.open_note_custom')}<kbd data-command="openNoteCustom"></kbd>
</li>
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
<span class="bx bx-code"></span> ${t('note_actions.note_source')}<kbd data-command="showNoteSource"></kbd>
</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
<span class="bx bx-save"></span> ${t('note_actions.save_revision')}<kbd data-command="forceSaveRevision"></kbd>
</li>
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t('note_actions.delete_note')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
<span class="bx bx-paperclip"></span> ${t('note_actions.note_attachments')}<kbd data-command="showAttachments"></kbd>
</li>
</div>
</div>`;
@@ -127,18 +176,18 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
}
async convertNoteIntoAttachmentCommand() {
if (!await dialogService.confirm(`Are you sure you want to convert note '${this.note.title}' into an attachment of the parent note?`)) {
if (!await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title }))) {
return;
}
const {attachment: newAttachment} = await server.post(`notes/${this.noteId}/convert-to-attachment`);
if (!newAttachment) {
toastService.showMessage(`Converting note '${this.note.title}' failed.`);
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
return;
}
toastService.showMessage(`Note '${newAttachment.title}' has been converted to attachment.`);
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, {
viewScope: {

View File

@@ -1,5 +1,6 @@
import { t } from "../../services/i18n.js";
import BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
const TPL = `
<div style="display: none;">
@@ -34,6 +35,6 @@ export default class UpdateAvailableWidget extends BasicWidget {
}
updateVersionStatus(latestVersion) {
this.$widget.toggle(latestVersion > glob.triliumVersion);
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
}
}

View File

@@ -8,7 +8,11 @@ export default class Container extends BasicWidget {
renderChildren() {
for (const widget of this.children) {
this.$widget.append(widget.render());
try {
this.$widget.append(widget.render());
} catch (e) {
widget.logRenderingError(e);
}
}
}
}

View File

@@ -216,7 +216,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
this.$tabContainer.empty();
for (const ribbonWidget of this.ribbonWidgets) {
const ret = ribbonWidget.getTitle(note);
const ret = await ribbonWidget.getTitle(note);
if (!ret.show) {
continue;
@@ -351,6 +351,21 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
noteTypeMimeChangedEvent() {
// We are ignoring the event which triggers a refresh since it is usually already done by a different
// event and causing a race condition in which the items appear twice.
}
/**
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
*
* <p>
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
*/
readOnlyTemporarilyDisabledEvent() {
this.refresh();
}
getActiveRibbonWidget() {
return this.ribbonWidgets.find(ch => ch.componentId === this.lastActiveComponentId)
}

View File

@@ -25,7 +25,7 @@ const TPL = `
</div>
<div class="delete-notes-list-wrapper">
<h4>${t('delete_notes.notes_to_be_deleted')} (<span class="deleted-notes-count"></span>)</h4>
<h4>${t('delete_notes.notes_to_be_deleted', { noteCount: '<span class="deleted-notes-count"></span>' })}</h4>
<ul class="delete-notes-list" style="max-height: 200px; overflow: auto;"></ul>
</div>
@@ -36,7 +36,7 @@ const TPL = `
<div class="broken-relations-wrapper">
<div class="alert alert-danger">
<h4>${t('delete_notes.broken_relations_to_be_deleted')} (<span class="broke-relations-count"></span>)</h4>
<h4>${t('delete_notes.broken_relations_to_be_deleted', { relationCount: '<span class="broke-relations-count"></span>'})}</h4>
<ul class="broken-relations-list" style="max-height: 200px; overflow: auto;"></ul>
</div>
@@ -126,11 +126,11 @@ export default class DeleteNotesDialog extends BasicWidget {
for (const attr of response.brokenRelations) {
this.$brokenRelationsList.append(
$("<li>")
.append(`${t('delete_notes.note')} `)
.append(await linkService.createLink(attr.value))
.append(` ${t('delete_notes.to_be_deleted', {attrName: attr.name})} `)
.append(await linkService.createLink(attr.noteId))
$("<li>").html(t("delete_notes.deleted_relation_text", {
note: (await linkService.createLink(attr.value)).html(),
relation: `<code>${attr.name}</code>`,
source: (await linkService.createLink(attr.noteId)).html()
}))
);
}
}

View File

@@ -58,6 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget {
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
allowCreatingNotes: true,
hideGoToSelectedNoteButton: true,
allowSearchNotes: true,
container: this.$results
})
// clear any event listener added in previous invocation of this function

View File

@@ -54,7 +54,7 @@ const TPL = `
data-bs-toggle="dropdown" data-bs-display="static">
</button>
<div class="revision-list dropdown-menu" style="position: static; height: 100%; overflow: auto;"></div>
<div class="revision-list dropdown-menu static" style="position: static; height: 100%; overflow: auto;"></div>
</div>
<div class="revision-content-wrapper">

View File

@@ -5,6 +5,7 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import FindInText from "./find_in_text.js";
import FindInCode from "./find_in_code.js";
import FindInHtml from "./find_in_html.js";
@@ -16,27 +17,26 @@ const waitForEnter = (findWidgetDelayMillis < 0);
// the focusout handler is called with relatedTarget equal to the label instead
// of undefined. It's -1 instead of > 0, so they don't tabstop
const TPL = `
<div style="contain: none;">
<div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);">
<style>
.find-widget-box {
padding: 10px;
border-top: 1px solid var(--main-border-color);
.find-widget-box, .replace-widget-box {
padding: 2px 10px 2px 10px;
align-items: center;
}
.find-widget-box > * {
.find-widget-box > *, .replace-widget-box > *{
margin-right: 15px;
}
.find-widget-box {
.find-widget-box, .replace-widget-box {
display: flex;
}
.find-widget-found-wrapper {
font-weight: bold;
}
.find-widget-search-term-input-group {
.find-widget-search-term-input-group, .replace-widget-replacetext-input {
max-width: 300px;
}
@@ -47,19 +47,23 @@ const TPL = `
<div class="find-widget-box">
<div class="input-group find-widget-search-term-input-group">
<input type="text" class="form-control find-widget-search-term-input">
<input type="text" class="form-control find-widget-search-term-input" placeholder="${t('find.find_placeholder')}">
<button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
<button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
<label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label>
<label tabIndex="-1" class="form-check-label">
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
${t('find.case_sensitive')}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
<label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label>
<label tabIndex="-1" class="form-check-label">
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
${t('find.match_words')}
</label>
</div>
<div class="find-widget-found-wrapper">
@@ -72,6 +76,12 @@ const TPL = `
<div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div>
</div>
<div class="replace-widget-box" style='display: none'>
<input type="text" class="form-control replace-widget-replacetext-input" placeholder="${t('find.replace_placeholder')}">
<button class="btn btn-sm replace-widget-replaceall-button" type="button">${t('find.replace_all')}</button>
<button class="btn btn-sm replace-widget-replace-button" type="button">${t('find.replace')}</button>
</div>
</div>`;
export default class FindWidget extends NoteContextAwareWidget {
@@ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$findBox = this.$widget.find('.find-widget-box');
this.$findBox.hide();
this.$widget.hide();
this.$input = this.$widget.find('.find-widget-search-term-input');
this.$currentFound = this.$widget.find('.find-widget-current-found');
this.$totalFound = this.$widget.find('.find-widget-total-found');
@@ -109,6 +118,13 @@ export default class FindWidget extends NoteContextAwareWidget {
this.$closeButton = this.$widget.find(".find-widget-close-button");
this.$closeButton.on("click", () => this.closeSearch());
this.$replaceWidgetBox = this.$widget.find(".replace-widget-box");
this.$replaceTextInput = this.$widget.find(".replace-widget-replacetext-input");
this.$replaceAllButton = this.$widget.find(".replace-widget-replaceall-button");
this.$replaceAllButton.on("click", () => this.replaceAll());
this.$replaceButton = this.$widget.find(".replace-widget-replace-button");
this.$replaceButton.on("click", () => this.replace());
this.$input.keydown(async e => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) {
// If ctrl+f is pressed when the findbox is shown, select the
@@ -121,7 +137,7 @@ export default class FindWidget extends NoteContextAwareWidget {
}
});
this.$findBox.keydown(async e => {
this.$widget.keydown(async e => {
if (e.key === 'Escape') {
await this.closeSearch();
}
@@ -142,13 +158,25 @@ export default class FindWidget extends NoteContextAwareWidget {
}
this.handler = await this.getHandler();
const isReadOnly = await this.noteContext.isReadOnly();
const selectedText = window.getSelection().toString() || "";
this.$findBox.show();
let selectedText = '';
if (this.note.type === 'code' && !isReadOnly){
const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection();
}else{
selectedText = window.getSelection().toString() || "";
}
this.$widget.show();
this.$input.focus();
if (['text', 'code'].includes(this.note.type) && !isReadOnly) {
this.$replaceWidgetBox.show();
}else{
this.$replaceWidgetBox.hide();
}
const isAlreadyVisible = this.$findBox.is(":visible");
const isAlreadyVisible = this.$widget.is(":visible");
if (isAlreadyVisible) {
if (selectedText) {
@@ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget {
}
async closeSearch() {
if (this.$findBox.is(":visible")) {
this.$findBox.hide();
if (this.$widget.is(":visible")) {
this.$widget.hide();
// Restore any state, if there's a current occurrence clear markers
// and scroll to and select the last occurrence
@@ -268,13 +296,27 @@ export default class FindWidget extends NoteContextAwareWidget {
}
}
async replace() {
const replaceText = this.$replaceTextInput.val();
await this.handler.replace(replaceText);
}
async replaceAll() {
const replaceText = this.$replaceTextInput.val();
await this.handler.replaceAll(replaceText);
}
isEnabled() {
return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type);
}
async entitiesReloadedEvent({loadResults}) {
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
this.$totalFound.text("?")
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly'))
&& attributeService.isAffecting(attr, this.note))) {
this.closeSearch();
}
}
}

View File

@@ -170,4 +170,55 @@ export default class FindInCode {
codeEditor.focus();
}
async replace(replaceText) {
// this.findResult may be undefined and null
if (!this.findResult || this.findResult.length===0){
return;
}
let currentFound = -1;
this.findResult.forEach((marker, index) => {
const pos = marker.find();
if (pos) {
if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
currentFound = index;
return;
}
}
});
if (currentFound >= 0) {
let marker = this.findResult[currentFound];
let pos = marker.find();
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
doc.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
let nextFound;
if (currentFound === this.findResult.length - 1) {
nextFound = 0;
} else {
nextFound = currentFound;
}
this.findResult.splice(currentFound, 1);
if (this.findResult.length > 0) {
this.findNext(0, nextFound, nextFound);
}
}
}
async replaceAll(replaceText) {
if (!this.findResult || this.findResult.length===0){
return;
}
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
codeEditor.operation(() => {
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound];
let pos = marker.find();
doc.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
}
});
this.findResult = [];
}
}

View File

@@ -21,6 +21,7 @@ export default class FindInText {
const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing');
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state;
if (searchTerm !== "") {
// Parameters are callback/text, options.matchCase=false, options.wholeWords=false
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
@@ -29,7 +30,7 @@ export default class FindInText {
// let re = new RegExp(searchTerm, 'gi');
// let m = text.match(re);
// totalFound = m ? m.length : 0;
const options = { "matchCase" : matchCase, "wholeWords" : wholeWord };
const options = { "matchCase": matchCase, "wholeWords": wholeWord };
findResult = textEditor.execute('find', searchTerm, options);
totalFound = findResult.results.length;
// Find the result beyond the cursor
@@ -102,4 +103,18 @@ export default class FindInText {
textEditor.focus();
}
async replace(replaceText) {
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
const textEditor = await this.getTextEditor();
textEditor.execute('replace', replaceText, this.editingState.highlightedResult);
}
}
async replaceAll(replaceText) {
if (this.editingState !== undefined && this.editingState.results.length > 0) {
const textEditor = await this.getTextEditor();
textEditor.execute('replaceAll', replaceText, this.editingState.results);
}
}
}

View File

@@ -5171,7 +5171,7 @@ const icons = [
"type_of_icon": "REGULAR"
},
{
"name": '_share',
"name": "share",
"slug": "share-regular",
"category_id": 101,
"type_of_icon": "REGULAR"
@@ -6826,7 +6826,7 @@ const icons = [
"type_of_icon": "SOLID"
},
{
"name": '_share',
"name": "share",
"slug": "share-solid",
"category_id": 101,
"type_of_icon": "SOLID"

View File

@@ -50,6 +50,9 @@ class NoteContextAwareWidget extends BasicWidget {
/**
* @inheritdoc
*
* <p>
* If the widget is not enabled, it will not receive `refreshWithNote` updates.
*
* @returns {boolean} true when an active note exists
*/
isEnabled() {

View File

@@ -30,6 +30,7 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js";
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
const TPL = `
<div class="note-detail">
@@ -255,6 +256,19 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
}
const {assetPath} = window.glob;
const cssToLoad = [
`${assetPath}/node_modules/codemirror/lib/codemirror.css`,
`${assetPath}/libraries/ckeditor/ckeditor-content.css`,
`${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`,
`${assetPath}/node_modules/katex/dist/katex.min.css`,
`${assetPath}/stylesheets/print.css`,
`${assetPath}/stylesheets/relation_map.css`,
`${assetPath}/stylesheets/ckeditor-theme.css`
];
if (isSyntaxHighlightEnabled()) {
cssToLoad.push(getStylesheetUrl("default:vs"));
}
this.$widget.find('.note-detail-printable:visible').printThis({
header: $("<div>")
@@ -273,15 +287,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
</script>
`,
importCSS: false,
loadCSS: [
`${assetPath}/node_modules/codemirror/lib/codemirror.css`,
`${assetPath}/libraries/ckeditor/ckeditor-content.css`,
`${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`,
`${assetPath}/node_modules/katex/dist/katex.min.css`,
`${assetPath}/stylesheets/print.css`,
`${assetPath}/stylesheets/relation_map.css`,
`${assetPath}/stylesheets/ckeditor-theme.css`
],
loadCSS: cssToLoad,
debug: true
});
}

View File

@@ -666,8 +666,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}
const node = this.prepareNode(branch);
noteList.push(node);
if (node) {
noteList.push(node);
}
}
return noteList;
@@ -709,7 +710,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const note = branch.getNoteFromCache();
if (!note) {
throw new Error(`Branch '${branch.branchId}' has no child note '${branch.noteId}'`);
console.warn(`Branch '${branch.branchId}' has no child note '${branch.noteId}'`);
return null;
}
const title = `${branch.prefix ? (`${branch.prefix} - `) : ""}${note.title}`;
@@ -1031,7 +1033,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
activeNode.load(true);
activeNode.setExpanded(true, {noAnimation: true});
toastService.showMessage("Saved search note refreshed.");
toastService.showMessage(t("note_tree.saved-search-note-refreshed"));
}
async batchUpdate(cb) {
@@ -1075,7 +1077,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
node.setExpanded(false);
if (noneCollapsedYet) {
toastService.showMessage("Auto collapsing notes after inactivity...");
toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity"));
noneCollapsedYet = false;
}
}

View File

@@ -101,7 +101,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
this.$noteTypeDropdown.append($typeLink);
}
for (const mimeType of await mimeTypesService.getMimeTypes()) {
for (const mimeType of mimeTypesService.getMimeTypes()) {
if (!mimeType.enabled) {
continue;
}
@@ -128,7 +128,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
async findTypeTitle(type, mime) {
if (type === 'code') {
const mimeTypes = await mimeTypesService.getMimeTypes();
const mimeTypes = mimeTypesService.getMimeTypes();
const found = mimeTypes.find(mt => mt.mime === mime);
return found ? found.title : mime;
@@ -159,7 +159,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
return true;
}
return await dialogService.confirm("It is not recommended to change note type when note content is not empty. Do you want to continue anyway?");
return await dialogService.confirm(t("note_types.confirm-change"));
}
async entitiesReloadedEvent({ loadResults }) {

View File

@@ -0,0 +1,83 @@
import { t } from "../../services/i18n.js";
import options from "../../services/options.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
const TPL = `\
<div class="classic-toolbar-widget"></div>
<style>
.classic-toolbar-widget {
--ck-color-toolbar-background: transparent;
--ck-color-button-default-background: transparent;
--ck-color-button-default-disabled-background: transparent;
min-height: 39px;
}
.classic-toolbar-widget .ck.ck-toolbar {
border: none;
}
.classic-toolbar-widget .ck.ck-button.ck-disabled {
opacity: 0.3;
}
body.mobile .classic-toolbar-widget {
position: relative;
overflow-x: auto;
}
body.mobile .classic-toolbar-widget .ck.ck-toolbar {
position: absolute;
}
</style>
`;
/**
* Handles the editing toolbar when the CKEditor is in decoupled mode.
*
* <p>
* This toolbar is only enabled if the user has selected the classic CKEditor.
*
* <p>
* The ribbon item is active by default for text notes, as long as they are not in read-only mode.
*/
export default class ClassicEditorToolbar extends NoteContextAwareWidget {
get name() {
return "classicEditor";
}
get toggleCommand() {
return "toggleRibbonTabClassicEditor";
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
}
async getTitle() {
return {
show: await this.#shouldDisplay(),
activate: true,
title: t("classic_editor_toolbar.title"),
icon: "bx bx-text"
};
}
async #shouldDisplay() {
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
return false;
}
if (this.note.type !== "text") {
return false;
}
if (await this.noteContext.isReadOnly()) {
return false;
}
return true;
}
}

View File

@@ -42,6 +42,10 @@ const TPL = `
word-break:keep-all;
white-space: nowrap;
}
.promoted-attribute-cell input[type="checkbox"] {
height: 1.5em;
}
</style>
<div class="promoted-attributes-container"></div>

View File

@@ -1,4 +1,6 @@
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import toastService from "../services/toast.js";
import { t } from "../services/i18n.js";
const WIDGET_TPL = `
<div class="card widget">
@@ -54,7 +56,9 @@ class RightPanelWidget extends NoteContextAwareWidget {
this.$buttons.append(buttonWidget.render());
}
this.initialized = this.doRenderBody();
this.initialized = this.doRenderBody().catch(e => {
this.logRenderingError(e);
});
}
/**

View File

@@ -61,9 +61,9 @@ export default class SearchString extends AbstractSearchOption {
await this.setAttribute('label', 'searchString', searchString);
if (this.note.title.startsWith('Search: ')) {
if (this.note.title.startsWith(t("search_string.search_prefix"))) {
await server.put(`notes/${this.note.noteId}/title`, {
title: `Search: ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}`}`
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}`}`
});
}
}, 1000);

View File

@@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js";
import utils from "../services/utils.js";
@@ -37,11 +38,11 @@ const TAB_TPL = `
<div class="note-tab-drag-handle"></div>
<div class="note-tab-icon"></div>
<div class="note-tab-title"></div>
<div class="note-tab-close bx bx-x" title="Close tab" data-trigger-command="closeActiveTab"></div>
<div class="note-tab-close bx bx-x" title="${t('tab_row.close_tab')}" data-trigger-command="closeActiveTab"></div>
</div>
</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="Add new tab">+</div>`;
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="${t('tab_row.add_new_tab')}">+</div>`;
const FILLER_TPL = `<div class="tab-row-filler"></div>`;
const TAB_ROW_TPL = `
@@ -258,10 +259,19 @@ export default class TabRowWidget extends BasicWidget {
x: e.pageX,
y: e.pageY,
items: [
{title: "Close", command: "closeTab", uiIcon: "bx bx-x"},
{title: "Close other tabs", command: "closeOtherTabs", uiIcon: "bx bx-x"},
{title: "Close all tabs", command: "closeAllTabs", uiIcon: "bx bx-x"},
{title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}
{title: t('tab_row.close'), command: "closeTab", uiIcon: "bx bx-x"},
{title: t('tab_row.close_other_tabs'), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1},
{title: t('tab_row.close_right_tabs'), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.at(-1).ntxId !== ntxId},
{title: t('tab_row.close_all_tabs'), command: "closeAllTabs", uiIcon: "bx bx-empty"},
{title: "----"},
{title: t('tab_row.reopen_last_tab'), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0},
{title: "----"},
{title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"},
{title: t('tab_row.copy_tab_to_new_window'), command: "copyTabToNewWindow", uiIcon: "bx bx-empty"}
],
selectMenuItemHandler: ({command}) => {
this.triggerCommand(command, {ntxId});
@@ -387,7 +397,7 @@ export default class TabRowWidget extends BasicWidget {
this.$newTab.before($tab);
this.setVisibility();
this.setTabCloseEvent($tab);
this.updateTitle($tab, 'New tab');
this.updateTitle($tab, t('tab_row.new_tab'));
this.cleanUpPreviouslyDraggedTabs();
this.layoutTabs();
this.setupDraggabilly();
@@ -672,7 +682,7 @@ export default class TabRowWidget extends BasicWidget {
const {note} = noteContext;
if (!note) {
this.updateTitle($tab, 'New tab');
this.updateTitle($tab, t('tab_row.new_tab'));
return;
}

View File

@@ -13,7 +13,7 @@
* to the wrong heading (although what "right" means in those cases is not
* clear), but it won't crash.
*/
import { t } from "../services/i18n.js";
import attributeService from "../services/attributes.js";
import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js";
@@ -55,14 +55,14 @@ const TPL = `<div class="toc-widget">
export default class TocWidget extends RightPanelWidget {
get widgetTitle() {
return "Table of Contents";
return t("toc.table_of_contents");
}
get widgetButtons() {
return [
new OnClickButtonWidget()
.icon("bx-cog")
.title("Options")
.title(t("toc.options"))
.titlePlacement("left")
.onClick(() => appContext.tabManager.openContextWithNote('_optionsTextNotes', {activate: true}))
.class("icon-action"),
@@ -125,18 +125,18 @@ export default class TocWidget extends RightPanelWidget {
*
* @param {string} html Note's html content
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX.
*/
*/
async replaceMathTextWithKatax(html) {
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
var matches = [...html.matchAll(mathTextRegex)];
let modifiedText = html;
if (matches.length > 0) {
// Process all matches asynchronously
for (const match of matches) {
let latexCode = match[1];
let rendered;
try {
rendered = katex.renderToString(latexCode, {
throwOnError: false
@@ -158,7 +158,7 @@ export default class TocWidget extends RightPanelWidget {
rendered = match[0]; // Fall back to original on error
}
}
// Replace the matched formula in the modified text
modifiedText = modifiedText.replace(match[0], rendered);
}

View File

@@ -0,0 +1,120 @@
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import options from "../../services/options.js";
/**
* An abstract {@link TypeWidget} which implements the CodeMirror editor, meant to be used as a parent for
* widgets requiring the editor.
*
* The widget handles the loading and initialization of the CodeMirror editor, as well as some common
* actions.
*
* The derived class must:
*
* - Define `$editor` in the constructor.
* - Call `super.doRender()` in the extended class.
* - Call `this._update(note, content)` in `#doRefresh(note)`.
*/
export default class AbstractCodeTypeWidget extends TypeWidget {
doRender() {
this.initialized = this.#initEditor();
}
async #initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = `${window.glob.assetPath}/node_modules/codemirror/mode/%N/%N.js`;
CodeMirror.modeInfo.find(mode=>mode.name === "JavaScript").mimes.push(...["application/javascript;env=frontend", "application/javascript;env=backend"]);
CodeMirror.modeInfo.find(mode=>mode.name === "SQLite").mimes=["text/x-sqlite", "text/x-sqlite;schema=trilium"];
this.codeEditor = CodeMirror(this.$editor[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
lineNumbers: true,
// we line wrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap, there's no horizontal scrollbar so no problem
lineWrapping: options.is('codeLineWrapEnabled'),
...this.getExtraOpts()
});
this.onEditorInitialized();
}
/**
* Can be extended in derived classes to add extra options to the CodeMirror constructor. The options are appended
* at the end, so it is possible to override the default values introduced by the abstract editor as well.
*
* @returns the extra options to be passed to the CodeMirror constructor.
*/
getExtraOpts() {
return {};
}
/**
* Called as soon as the CodeMirror library has been loaded and the editor was constructed. Can be extended in
* derived classes to add additional functionality or to register event handlers.
*
* By default, it does nothing.
*/
onEditorInitialized() {
// Do nothing by default.
}
/**
* Must be called by the derived classes in `#doRefresh(note)` in order to react to changes.
*
* @param {*} note the note that was changed.
* @param {*} content the new content of the note.
*/
_update(note, content) {
// CodeMirror breaks pretty badly on null, so even though it shouldn't happen (guarded by a consistency check)
// we provide fallback
this.codeEditor.setValue(content || "");
this.codeEditor.clearHistory();
let info = CodeMirror.findModeByMIME(note.mime);
if (!info) {
// Switch back to plain text if CodeMirror does not have a mode for whatever MIME type we're editing.
// To avoid inheriting a mode from a previously open code note.
info = CodeMirror.findModeByMIME("text/plain");
}
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
};
show() {
this.$widget.show();
if (this.codeEditor) { // show can be called before render
this.codeEditor.refresh();
}
}
focus() {
this.$editor.focus();
this.codeEditor.focus();
}
scrollToEnd() {
this.codeEditor.setCursor(this.codeEditor.lineCount(), 0);
this.codeEditor.focus();
}
cleanup() {
if (this.codeEditor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {
this.codeEditor.setValue('');
});
}
}
}

View File

@@ -4,8 +4,15 @@ import froca from "../../services/froca.js";
import linkService from "../../services/link.js";
import contentRenderer from "../../services/content_renderer.js";
import utils from "../../services/utils.js";
import options from "../../services/options.js";
export default class AbstractTextTypeWidget extends TypeWidget {
doRender() {
super.doRender();
this.refreshCodeBlockOptions();
}
setupImageOpening(singleClickOpens) {
this.$widget.on("dblclick", "img", e => this.openImageInCurrentTab($(e.target)));
@@ -25,7 +32,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
async openImageInCurrentTab($img) {
const { noteId, viewScope } = await this.parseFromImage($img);
if (noteId) {
appContext.tabManager.getActiveContext().setNote(noteId, { viewScope });
} else {
@@ -33,8 +40,8 @@ export default class AbstractTextTypeWidget extends TypeWidget {
}
}
openImageInNewTab($img) {
const { noteId, viewScope } = this.parseFromImage($img);
async openImageInNewTab($img) {
const { noteId, viewScope } = await this.parseFromImage($img);
if (noteId) {
appContext.tabManager.openTabWithNoteWithHoisting(noteId, { viewScope });
@@ -108,4 +115,16 @@ export default class AbstractTextTypeWidget extends TypeWidget {
});
}
}
refreshCodeBlockOptions() {
const wordWrap = options.is("codeBlockWordWrap");
this.$widget.toggleClass("word-wrap", wordWrap);
}
async entitiesReloadedEvent({loadResults}) {
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
this.refreshCodeBlockOptions();
}
}
}

View File

@@ -0,0 +1,354 @@
/*
* This code is an adaptation of https://github.com/antoniotejada/Trilium-SyntaxHighlightWidget with additional improvements, such as:
*
* - support for selecting the language manually;
* - support for determining the language automatically, if a special language is selected ("Auto-detected");
* - limit for highlighting.
*
* TODO: Generally this class can be done directly in the CKEditor repository.
*/
import library_loader from "../../../services/library_loader.js";
import mime_types from "../../../services/mime_types.js";
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
export async function initSyntaxHighlighting(editor) {
if (!isSyntaxHighlightEnabled) {
return;
}
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
initTextEditor(editor);
}
const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
const tag = "SyntaxHighlightWidget";
const debugLevels = ["error", "warn", "info", "log", "debug"];
const debugLevel = "debug";
let warn = function() {};
if (debugLevel >= debugLevels.indexOf("warn")) {
warn = console.warn.bind(console, tag + ": ");
}
let info = function() {};
if (debugLevel >= debugLevels.indexOf("info")) {
info = console.info.bind(console, tag + ": ");
}
let log = function() {};
if (debugLevel >= debugLevels.indexOf("log")) {
log = console.log.bind(console, tag + ": ");
}
let dbg = function() {};
if (debugLevel >= debugLevels.indexOf("debug")) {
dbg = console.debug.bind(console, tag + ": ");
}
function assert(e, msg) {
console.assert(e, tag + ": " + msg);
}
// TODO: Should this be scoped to note?
let markerCounter = 0;
function initTextEditor(textEditor) {
log("initTextEditor");
let widget = this;
const document = textEditor.model.document;
// Create a conversion from model to view that converts
// hljs:hljsClassName:uniqueId into a span with hljsClassName
// See the list of hljs class names at
// https://github.com/highlightjs/highlight.js/blob/6b8c831f00c4e87ecd2189ebbd0bb3bbdde66c02/docs/css-classes-reference.rst
textEditor.conversion.for('editingDowncast').markerToHighlight( {
model: "hljs",
view: ( { markerName } ) => {
dbg("markerName " + markerName);
// markerName has the pattern addMarker:cssClassName:uniqueId
const [ , cssClassName, id ] = markerName.split( ':' );
// The original code at
// https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
// has this comment
// Marker removal from the view has a bug:
// https://github.com/ckeditor/ckeditor5/issues/7499
// A minimal option is to return a new object for each converted marker...
return {
name: 'span',
classes: [ cssClassName ],
attributes: {
// ...however, adding a unique attribute should be future-proof..
'data-syntax-result': id
},
};
}
});
// XXX This is done at BalloonEditor.create time, so it assumes this
// document is always attached to this textEditor, empirically that
// seems to be the case even with two splits showing the same note,
// it's not clear if CKEditor5 has apis to attach and detach
// documents around
document.registerPostFixer(function(writer) {
log("postFixer");
// Postfixers are a simpler way of tracking changes than onchange
// See
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
const changes = document.differ.getChanges();
let dirtyCodeBlocks = new Set();
for (const change of changes) {
dbg("change " + JSON.stringify(change));
if ((change.type == "insert") && (change.name == "codeBlock")) {
// A new code block was inserted
const codeBlock = change.position.nodeAfter;
// Even if it's a new codeblock, it needs dirtying in case
// it already has children, like when pasting one or more
// full codeblocks, undoing a delete, changing the language,
// etc (the postfixer won't get later changes for those).
log("dirtying inserted codeBlock " + JSON.stringify(codeBlock.toJSON()));
dirtyCodeBlocks.add(codeBlock);
} else if (change.type == "remove" && (change.name == "codeBlock")) {
// An existing codeblock was removed, do nothing. Note the
// node is no longer in the editor so the codeblock cannot
// be inspected here. No need to dirty the codeblock since
// it has been removed
log("removing codeBlock at path " + JSON.stringify(change.position.toJSON()));
} else if (((change.type == "remove") || (change.type == "insert")) &&
change.position.parent.is('element', 'codeBlock')) {
// Text was added or removed from the codeblock, force a
// highlight
const codeBlock = change.position.parent;
log("dirtying codeBlock " + JSON.stringify(codeBlock.toJSON()));
dirtyCodeBlocks.add(codeBlock);
}
}
for (let codeBlock of dirtyCodeBlocks) {
highlightCodeBlock(codeBlock, writer);
}
// Adding markers doesn't modify the document data so no need for
// postfixers to run again
return false;
});
// This assumes the document is empty and a explicit call to highlight
// is not necessary here. Empty documents have a single children of type
// paragraph with no text
assert((document.getRoot().childCount == 1) &&
(document.getRoot().getChild(0).name == "paragraph") &&
document.getRoot().getChild(0).isEmpty);
}
/**
* This implements highlighting via ephemeral markers (not stored in the
* document).
*
* XXX Another option would be to use formatting markers, which would have
* the benefit of making it work for readonly notes. On the flip side,
* the formatting would be stored with the note and it would need a
* way to remove that formatting when editing back the note.
*/
function highlightCodeBlock(codeBlock, writer) {
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
const model = codeBlock.root.document.model;
// Can't invoke addMarker with an already existing marker name,
// clear all highlight markers first. Marker names follow the
// pattern hljs:cssClassName:uniqueId, eg hljs:hljs-comment:1
const codeBlockRange = model.createRangeIn(codeBlock);
for (const marker of model.markers.getMarkersIntersectingRange(codeBlockRange)) {
dbg("removing marker " + marker.name);
writer.removeMarker(marker.name);
}
// Don't highlight if plaintext (note this needs to remove the markers
// above first, in case this was a switch from non plaintext to
// plaintext)
const mimeType = codeBlock.getAttribute("language");
if (mimeType == "text-plain") {
// XXX There's actually a plaintext language that could be used
// if you wanted the non-highlight formatting of
// highlight.js css applied, see
// https://github.com/highlightjs/highlight.js/issues/700
log("not highlighting plaintext codeblock");
return;
}
// Find the corresponding language for the given mimetype.
const highlightJsLanguage = mime_types.getHighlightJsNameForMime(mimeType);
if (mimeType !== mime_types.MIME_TYPE_AUTO && !highlightJsLanguage) {
console.warn(`Unsupported highlight.js for mime type ${mimeType}.`);
return;
}
// Don't highlight if the code is too big, as the typing performance will be highly degraded.
if (codeBlock.childCount >= HIGHLIGHT_MAX_BLOCK_COUNT) {
return;
}
// highlight.js needs the full text without HTML tags, eg for the
// text
// #include <stdio.h>
// the highlighted html is
// <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span>
// But CKEditor codeblocks have <br> instead of \n
// Do a two pass algorithm:
// - First pass collect the codeblock children text, change <br> to
// \n
// - invoke highlight.js on the collected text generating html
// - Second pass parse the highlighted html spans and match each
// char to the CodeBlock text. Issue addMarker CKEditor calls for
// each span
// XXX This is brittle and assumes how highlight.js generates html
// (blanks, which characters escapes, etc), a better approach
// would be to use highlight.js beta api TreeTokenizer?
// Collect all the text nodes to pass to the highlighter Text is
// direct children of the codeBlock
let text = "";
for (let i = 0; i < codeBlock.childCount; ++i) {
let child = codeBlock.getChild(i);
// We only expect text and br elements here
if (child.is("$text")) {
dbg("child text " + child.data);
text += child.data;
} else if (child.is("element") &&
(child.name == "softBreak")) {
dbg("softBreak");
text += "\n";
} else {
warn("Unkown child " + JSON.stringify(child.toJSON()));
}
}
let highlightRes;
if (mimeType === mime_types.MIME_TYPE_AUTO) {
highlightRes = hljs.highlightAuto(text);
} else {
highlightRes = hljs.highlight(text, { language: highlightJsLanguage });
}
dbg("text\n" + text);
dbg("html\n" + highlightRes.value);
let iHtml = 0;
let html = highlightRes.value;
let spanStack = [];
let iChild = -1;
let childText = "";
let child = null;
let iChildText = 0;
while (iHtml < html.length) {
// Advance the text index and fetch a new child if necessary
if (iChildText >= childText.length) {
iChild++;
if (iChild < codeBlock.childCount) {
dbg("Fetching child " + iChild);
child = codeBlock.getChild(iChild);
if (child.is("$text")) {
dbg("child text " + child.data);
childText = child.data;
iChildText = 0;
} else if (child.is("element", "softBreak")) {
dbg("softBreak");
iChildText = 0;
childText = "\n";
} else {
warn("child unknown!!!");
}
} else {
// Don't bail if beyond the last children, since there's
// still html text, it must be a closing span tag that
// needs to be dealt with below
childText = "";
}
}
// This parsing is made slightly simpler and faster by only
// expecting <span> and </span> tags in the highlighted html
if ((html[iHtml] == "<") && (html[iHtml+1] != "/")) {
// new span, note they can be nested eg C preprocessor lines
// are inside a hljs-meta span, hljs-title function names
// inside a hljs-function span, etc
let iStartQuot = html.indexOf("\"", iHtml+1);
let iEndQuot = html.indexOf("\"", iStartQuot+1);
let className = html.slice(iStartQuot+1, iEndQuot);
// XXX highlight js uses scope for Python "title function_",
// etc for now just use the first style only
// See https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html#a-note-on-scopes-with-sub-scopes
let iBlank = className.indexOf(" ");
if (iBlank > 0) {
className = className.slice(0, iBlank);
}
dbg("Found span start " + className);
iHtml = html.indexOf(">", iHtml) + 1;
// push the span
let posStart = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
spanStack.push({ "className" : className, "posStart": posStart});
} else if ((html[iHtml] == "<") && (html[iHtml+1] == "/")) {
// Done with this span, pop the span and mark the range
iHtml = html.indexOf(">", iHtml+1) + 1;
let stackTop = spanStack.pop();
let posStart = stackTop.posStart;
let className = stackTop.className;
let posEnd = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
let range = writer.createRange(posStart, posEnd);
let markerName = "hljs:" + className + ":" + markerCounter;
// Use an incrementing number for the uniqueId, random of
// 10000000 is known to cause collisions with a few
// codeblocks of 10s of lines on real notes (each line is
// one or more marker).
// Wrap-around for good measure so all numbers are positive
// XXX Another option is to catch the exception and retry or
// go through the markers and get the largest + 1
markerCounter = (markerCounter + 1) & 0xFFFFFF;
dbg("Found span end " + className);
dbg("Adding marker " + markerName + ": " + JSON.stringify(range.toJSON()));
writer.addMarker(markerName, {"range": range, "usingOperation": false});
} else {
// Text, we should also have text in the children
assert(
((iChild < codeBlock.childCount) && (iChildText < childText.length)),
"Found text in html with no corresponding child text!!!!"
);
if (html[iHtml] == "&") {
// highlight.js only encodes
// .replace(/&/g, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/>/g, '&gt;')
// .replace(/"/g, '&quot;')
// .replace(/'/g, '&#x27;');
// see https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/lib/utils.js#L5
let iAmpEnd = html.indexOf(";", iHtml);
dbg(html.slice(iHtml, iAmpEnd));
iHtml = iAmpEnd + 1;
} else {
// regular text
dbg(html[iHtml]);
iHtml++;
}
iChildText++;
}
}
}

View File

@@ -34,6 +34,8 @@ import BackendLogWidget from "./content/backend_log.js";
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
import RibbonOptions from "./options/appearance/ribbon.js";
import LocalizationOptions from "./options/appearance/i18n.js";
import CodeBlockOptions from "./options/appearance/code_block.js";
import EditorOptions from "./options/text_notes/editor.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style>
@@ -59,6 +61,7 @@ const CONTENT_WIDGETS = {
LocalizationOptions,
ThemeOptions,
FontsOptions,
CodeBlockOptions,
ZoomFactorOptions,
NativeTitleBarOptions,
MaxContentWidthOptions,
@@ -66,6 +69,7 @@ const CONTENT_WIDGETS = {
],
_optionsShortcuts: [ KeyboardShortcutsOptions ],
_optionsTextNotes: [
EditorOptions,
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,

View File

@@ -1,8 +1,7 @@
import { t } from "../../services/i18n.js";
import libraryLoader from "../../services/library_loader.js";
import TypeWidget from "./type_widget.js";
import keyboardActionService from "../../services/keyboard_actions.js";
import options from "../../services/options.js";
import AbstractCodeTypeWidget from "./abstract_code_type_widget.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@@ -21,53 +20,31 @@ const TPL = `
<div class="note-detail-code-editor"></div>
</div>`;
export default class EditableCodeTypeWidget extends TypeWidget {
export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
static getType() { return "editableCode"; }
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$editor = this.$widget.find('.note-detail-code-editor');
keyboardActionService.setupActionsForElement('code-detail', this.$widget, this);
super.doRender();
this.initialized = this.initEditor();
super.doRender();
}
async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = `${window.glob.assetPath}/node_modules/codemirror/mode/%N/%N.js`;
CodeMirror.modeInfo.find(mode=>mode.name === "JavaScript").mimes.push(...["application/javascript;env=frontend", "application/javascript;env=backend"]);
CodeMirror.modeInfo.find(mode=>mode.name === "SQLite").mimes=["text/x-sqlite", "text/x-sqlite;schema=trilium"];
this.codeEditor = CodeMirror(this.$editor[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
getExtraOpts() {
return {
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true,
tabindex: 300,
// we line wrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap, there's no horizontal scrollbar so no problem
lineWrapping: options.is('codeLineWrapEnabled'),
dragDrop: false, // with true the editor inlines dropped files which is not what we expect
placeholder: t('editable_code.placeholder'),
});
};
}
onEditorInitialized() {
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
}
@@ -75,57 +52,18 @@ export default class EditableCodeTypeWidget extends TypeWidget {
const blob = await this.note.getBlob();
await this.spacedUpdate.allowUpdateWithoutChange(() => {
// CodeMirror breaks pretty badly on null, so even though it shouldn't happen (guarded by a consistency check)
// we provide fallback
this.codeEditor.setValue(blob.content || "");
this.codeEditor.clearHistory();
let info = CodeMirror.findModeByMIME(note.mime);
if (!info) {
// Switch back to plain text if CodeMirror does not have a mode for whatever MIME type we're editing.
// To avoid inheriting a mode from a previously open code note.
info = CodeMirror.findModeByMIME("text/plain");
}
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
this._update(note, blob.content);
});
this.show();
}
show() {
this.$widget.show();
if (this.codeEditor) { // show can be called before render
this.codeEditor.refresh();
}
}
getData() {
return {
content: this.codeEditor.getValue()
};
}
focus() {
this.$editor.focus();
this.codeEditor.focus();
}
scrollToEnd() {
this.codeEditor.setCursor(this.codeEditor.lineCount(), 0);
this.codeEditor.focus();
}
cleanup() {
if (this.codeEditor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {
this.codeEditor.setValue('');
});
}
}
async executeWithCodeEditorEvent({resolve, ntxId}) {
if (!this.isNoteContext(ntxId)) {
return;

View File

@@ -10,6 +10,8 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import link from "../../services/link.js";
import appContext from "../../components/app_context.js";
import dialogService from "../../services/dialog.js";
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
import options from "../../services/options.js";
const ENABLE_INSPECTOR = false;
@@ -87,6 +89,29 @@ const TPL = `
</div>
`;
function buildListOfLanguages() {
const userLanguages = (mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => ({
language: mimeTypesService.normalizeMimeTypeForCKEditor(mt.mime),
label: mt.title
}));
return [
{
language: mimeTypesService.MIME_TYPE_AUTO,
label: t("editable-text.auto-detect-language")
},
...userLanguages
];
}
/**
* The editor can operate into two distinct modes:
*
* - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph).
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
*/
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
static getType() { return "editableText"; }
@@ -105,21 +130,17 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const isClassicEditor = (options.get("textNoteEditorType") === "ckeditor-classic")
const editorClass = (isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor);
const codeBlockLanguages =
(await mimeTypesService.getMimeTypes())
.filter(mt => mt.enabled)
.map(mt => ({
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
label: mt.title
}));
const codeBlockLanguages = buildListOfLanguages();
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
// display of $widget in both branches.
this.$widget.show();
this.watchdog = new EditorWatchdog(BalloonEditor, {
this.watchdog = new CKEditor.EditorWatchdog(editorClass, {
// An average number of milliseconds between the last editor errors (defaults to 5000).
// When the period of time between errors is lower than that and the crashNumberLimit
// is also reached, the watchdog changes its state to crashedPermanently, and it stops
@@ -155,7 +176,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
});
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
const editor = await BalloonEditor.create(elementOrData, editorConfig);
const editor = await editorClass.create(elementOrData, editorConfig);
await initSyntaxHighlighting(editor);
if (isClassicEditor) {
let $classicToolbarWidget;
if (!utils.isMobile()) {
const $parentSplit = this.$widget.parents(".note-split.type-text");
$classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
} else {
$classicToolbarWidget = $("body").find(".classic-toolbar-widget");
}
$classicToolbarWidget.empty();
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
}
editor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());

View File

@@ -18,14 +18,28 @@ const TPL = `
width: 130px;
text-align: center;
margin: 10px;
padding; 10px;
border: 1px transparent solid;
}
.workspace-notes .workspace-note:hover {
cursor: pointer;
border: 1px solid var(--main-border-color);
}
.note-detail-empty-results .aa-dropdown-menu {
max-height: 50vh;
overflow: scroll;
border: var(--bs-border-width) solid var(--bs-border-color);
border-top: 0;
}
.empty-tab-search .note-autocomplete-input {
border-bottom-left-radius: 0;
}
.empty-tab-search .input-clearer-button {
border-bottom-right-radius: 0;
}
.workspace-icon {
text-align: center;
@@ -33,14 +47,14 @@ const TPL = `
}
</style>
<div class="form-group">
<div class="workspace-notes"></div>
<div class="form-group empty-tab-search">
<label>${t('empty.open_note_instruction')}</label>
<div class="input-group">
<div class="input-group mt-1">
<input class="form-control note-autocomplete" placeholder="${t('empty.search_placeholder')}">
</div>
</div>
<div class="workspace-notes"></div>
<div class="note-detail-empty-results"></div>
</div>`;
export default class EmptyTypeWidget extends TypeWidget {
@@ -51,10 +65,13 @@ export default class EmptyTypeWidget extends TypeWidget {
this.$widget = $(TPL);
this.$autoComplete = this.$widget.find(".note-autocomplete");
this.$results = this.$widget.find(".note-detail-empty-results");
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
hideGoToSelectedNoteButton: true,
allowCreatingNotes: true
allowCreatingNotes: true,
allowSearchNotes: true,
container: this.$results
})
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
if (!suggestion.notePath) {
@@ -66,6 +83,7 @@ export default class EmptyTypeWidget extends TypeWidget {
this.$workspaceNotes = this.$widget.find('.workspace-notes');
noteAutocompleteService.showRecentNotes(this.$autoComplete);
super.doRender();
}

View File

@@ -0,0 +1,119 @@
import { t } from "../../../../services/i18n.js";
import library_loader from "../../../../services/library_loader.js";
import server from "../../../../services/server.js";
import OptionsWidget from "../options_widget.js";
const SAMPLE_LANGUAGE = "javascript";
const SAMPLE_CODE = `\
const n = 10;
greet(n); // Print "Hello World" for n times
/**
* Displays a "Hello World!" message for a given amount of times, on the standard console. The "Hello World!" text will be displayed once per line.
*
* @param {number} times The number of times to print the \`Hello World!\` message.
*/
function greet(times) {
for (let i = 0; i++; i < times) {
console.log("Hello World!");
}
}
`
const TPL = `
<div class="options-section">
<h4>${t("highlighting.title")}</h4>
<p>${t("highlighting.description")}</p>
<div class="form-group row">
<div class="col-6">
<label>${t("highlighting.color-scheme")}</label>
<select class="theme-select form-select"></select>
</div>
<div class="col-6 side-checkbox">
<label class="form-check">
<input type="checkbox" class="word-wrap form-check-input" />
${t("code_block.word_wrapping")}
</label>
</div>
</div>
<div class="form-group row">
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
</div>
</div>
<style>
.code-sample-wrapper {
margin-top: 1em;
}
</style>
</div>
`;
/**
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.
*/
export default class CodeBlockOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select");
this.$themeSelect.on("change", async () => {
const newTheme = this.$themeSelect.val();
library_loader.loadHighlightingTheme(newTheme);
await server.put(`options/codeBlockTheme/${newTheme}`);
});
this.$wordWrap = this.$widget.find("input.word-wrap");
this.$wordWrap.on("change", () => this.updateCheckboxOption("codeBlockWordWrap", this.$wordWrap));
// Set up preview
this.$sampleEl = this.$widget.find(".code-sample");
}
#setupPreview(shouldEnableSyntaxHighlight) {
const text = SAMPLE_CODE;
if (shouldEnableSyntaxHighlight) {
library_loader
.requireLibrary(library_loader.HIGHLIGHT_JS)
.then(() => {
const highlightedText = hljs.highlight(text, {
language: SAMPLE_LANGUAGE
});
this.$sampleEl.html(highlightedText.value);
});
} else {
this.$sampleEl.text(text);
}
}
async optionsLoaded(options) {
const themeGroups = await server.get("options/codeblock-themes");
this.$themeSelect.empty();
for (const [ key, themes ] of Object.entries(themeGroups)) {
const $group = (key ? $("<optgroup>").attr("label", key) : null);
for (const theme of themes) {
const option = $("<option>")
.attr("value", theme.val)
.text(theme.title);
if ($group) {
$group.append(option);
} else {
this.$themeSelect.append(option);
}
}
this.$themeSelect.append($group);
}
this.$themeSelect.val(options.codeBlockTheme);
this.setCheckboxState(this.$wordWrap, options.codeBlockWordWrap);
this.$widget.closest(".note-detail-printable").toggleClass("word-wrap", options.codeBlockWordWrap === "true");
this.#setupPreview(options.codeBlockTheme !== "none");
}
}

View File

@@ -133,14 +133,17 @@ export default class FontsOptions extends OptionsWidget {
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
}
isEnabled() {
return this._isEnabled;
}
async optionsLoaded(options) {
if (options.overrideThemeFonts !== 'true') {
this.toggleInt(false);
this._isEnabled = (options.overrideThemeFonts === 'true');
this.toggleInt(this._isEnabled);
if (!this._isEnabled) {
return;
}
this.toggleInt(true);
this.$mainFontSize.val(options.mainFontSize);
this.fillFontFamilyOptions(this.$mainFontFamily, options.mainFontFamily);

View File

@@ -2,6 +2,8 @@ import OptionsWidget from "../options_widget.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
const MIN_VALUE = 640;
const TPL = `
<div class="options-section">
<h4>${t("max_content_width.title")}</h4>
@@ -11,7 +13,7 @@ const TPL = `
<div class="form-group row">
<div class="col-6">
<label>${t("max_content_width.max_width_label")}</label>
<input type="number" min="200" step="10" class="max-content-width form-control options-number-input">
<input type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
</div>
</div>
@@ -34,6 +36,6 @@ export default class MaxContentWidthOptions extends OptionsWidget {
}
async optionsLoaded(options) {
this.$maxContentWidth.val(options.maxContentWidth);
this.$maxContentWidth.val(Math.max(MIN_VALUE, options.maxContentWidth));
}
}

View File

@@ -13,11 +13,11 @@ const TPL = `
<select class="theme-select form-select"></select>
</div>
<div class="col-6">
<label>${t("theme.override_theme_fonts_label")}</label>
<div class="form-check">
<div class="col-6 side-checkbox">
<label class="form-check">
<input type="checkbox" class="override-theme-fonts form-check-input">
</div>
${t("theme.override_theme_fonts_label")}
</label>
</div>
</div>
</div>`;

View File

@@ -42,7 +42,21 @@ const TPL = `
<div class="options-section">
<h4>${t('backup.existing_backups')}</h4>
<ul class="existing-backup-list"></ul>
<table class="table table-stripped">
<colgroup>
<col width="33%" />
<col />
</colgroup>
<thead>
<tr>
<th>${t("backup.date-and-time")}</th>
<th>${t("backup.path")}</th>
</tr>
</thead>
<tbody class="existing-backup-list-items">
</tbody>
</table>
</div>
`;
@@ -73,7 +87,7 @@ export default class BackupOptions extends OptionsWidget {
this.$monthlyBackupEnabled.on('change', () =>
this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
this.$existingBackupList = this.$widget.find(".existing-backup-list");
this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
}
optionsLoaded(options) {
@@ -85,11 +99,34 @@ export default class BackupOptions extends OptionsWidget {
this.$existingBackupList.empty();
if (!backupFiles.length) {
backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
this.$existingBackupList.append($(`
<tr>
<td class="empty-table-placeholder" colspan="2">${t('backup.no_backup_yet')}</td>
</tr>
`));
return;
}
// Sort the backup files by modification date & time in a desceding order
backupFiles.sort((a, b) => {
if (a.mtime < b.mtime) return 1;
if (a.mtime > b.mtime) return -1;
return 0;
});
const dateTimeFormatter = new Intl.DateTimeFormat(navigator.language, {
dateStyle: "medium",
timeStyle: "medium"
});
for (const {filePath, mtime} of backupFiles) {
this.$existingBackupList.append($("<li>").text(`${filePath} ${mtime ? ` - ${mtime}` : ''}`));
this.$existingBackupList.append($(`
<tr>
<td>${(mtime) ? dateTimeFormatter.format(new Date(mtime)) : "-"}</td>
<td>${filePath}</td>
</tr>
`));
}
});
}

View File

@@ -19,9 +19,24 @@ export default class CodeMimeTypesOptions extends OptionsWidget {
async optionsLoaded(options) {
this.$mimeTypes.empty();
let index = -1;
let prevInitial = "";
for (const mimeType of await mimeTypesService.getMimeTypes()) {
for (const mimeType of mimeTypesService.getMimeTypes()) {
const id = "code-mime-type-" + (idCtr++);
index++;
// Append a heading to group items by the first letter, excepting for the
// first item ("Plain Text"). Note: this code assumes the items are already
// in alphabetical ordered.
if (index > 0) {
const initial = mimeType.title.charAt(0).toUpperCase();
if (initial !== prevInitial) {
this.$mimeTypes.append($("<h5>").text(initial));
prevInitial = initial;
}
}
this.$mimeTypes.append($("<li>")
.append($('<input type="checkbox" class="form-check-input">')

View File

@@ -95,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").text(token.name))
.append($("<td>").text(token.utcDateCreated))
.append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>')
$(`<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>`)
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>')
$(`<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>`)
.on("click", () => this.deleteToken(token.etapiTokenId, token.name))
))
);

View File

@@ -44,6 +44,20 @@ export default class OptionsWidget extends NoteContextAwareWidget {
optionsLoaded(options) {}
async refresh() {
this.toggleInt(this.isEnabled());
try {
await this.refreshWithNote(this.note);
} catch (e) {
// Ignore errors when user is refreshing or navigating away.
if (e === "rejected by browser") {
return;
}
throw e;
}
}
async refreshWithNote(note) {
const options = await server.get('options');

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