diff --git a/apps/build-docs/package.json b/apps/build-docs/package.json index 47ed510b65..c1e1efa7eb 100644 --- a/apps/build-docs/package.json +++ b/apps/build-docs/package.json @@ -14,7 +14,7 @@ "keywords": [], "author": "Elian Doran ", "license": "AGPL-3.0-only", - "packageManager": "pnpm@10.30.2", + "packageManager": "pnpm@10.30.3", "devDependencies": { "@redocly/cli": "2.19.2", "archiver": "7.0.1", diff --git a/apps/client/package.json b/apps/client/package.json index bcc591818d..4a9b78bb67 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -22,6 +22,7 @@ "@fullcalendar/interaction": "6.1.20", "@fullcalendar/list": "6.1.20", "@fullcalendar/multimonth": "6.1.20", + "@fullcalendar/rrule": "6.1.20", "@fullcalendar/timegrid": "6.1.20", "@maplibre/maplibre-gl-leaflet": "0.1.3", "@mermaid-js/layout-elk": "0.2.0", @@ -63,6 +64,7 @@ "react-i18next": "16.5.4", "react-window": "2.2.7", "reveal.js": "5.2.1", + "rrule": "2.8.1", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", "vanilla-js-wheel-zoom": "9.0.4" diff --git a/apps/client/src/menus/electron_context_menu.ts b/apps/client/src/menus/electron_context_menu.ts index 219a009c7c..6baba6a951 100644 --- a/apps/client/src/menus/electron_context_menu.ts +++ b/apps/client/src/menus/electron_context_menu.ts @@ -3,6 +3,8 @@ import options from "../services/options.js"; import zoomService from "../components/zoom.js"; import contextMenu, { type MenuItem } from "./context_menu.js"; import { t } from "../services/i18n.js"; +import server from "../services/server.js"; +import * as clipboardExt from "../services/clipboard_ext.js"; import type { BrowserWindow } from "electron"; import type { CommandNames, AppContext } from "../components/app_context.js"; @@ -60,6 +62,33 @@ function setupContextMenu() { uiIcon: "bx bx-copy", handler: () => webContents.copy() }); + + items.push({ + enabled: hasText, + title: t("electron_context_menu.copy-as-markdown"), + uiIcon: "bx bx-copy-alt", + handler: async () => { + const selection = window.getSelection(); + if (!selection || !selection.rangeCount) return ''; + + const range = selection.getRangeAt(0); + const div = document.createElement('div'); + div.appendChild(range.cloneContents()); + + const htmlContent = div.innerHTML; + if (htmlContent) { + try { + const { markdownContent } = await server.post<{ markdownContent: string }>( + "other/to-markdown", + { htmlContent } + ); + await clipboardExt.copyTextWithToast(markdownContent); + } catch (error) { + console.error("Failed to copy as markdown:", error); + } + } + } + }); } if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") { diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index c8d8ffecac..a03a89892a 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -16,6 +16,8 @@ import protectedSessionHolder from "./protected_session_holder.js"; import renderService from "./render.js"; import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js"; import utils, { getErrorMessage } from "./utils.js"; +import PdfViewer from "../widgets/type_widgets/file/PdfViewer"; +import { h, render } from "preact"; let idCounter = 1; @@ -195,10 +197,13 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent: const $content = $('
'); if (type === "pdf") { - const $pdfPreview = $(''); - $pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`)); + const url = `../../api/${entityType}/${entityId}/open`; + const $viewer = $(`
`); + render(h(PdfViewer, {pdfUrl: url, editable: false}), $viewer.get(0)!); + + $content.append($viewer); + - $content.append($pdfPreview); } else if (type === "audio") { const $audioPreview = $("") .attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`)) diff --git a/apps/client/src/stylesheets/theme-next/notes/text.css b/apps/client/src/stylesheets/theme-next/notes/text.css index e025b23fb2..252a3da6b2 100644 --- a/apps/client/src/stylesheets/theme-next/notes/text.css +++ b/apps/client/src/stylesheets/theme-next/notes/text.css @@ -647,10 +647,10 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child { font-weight: 600; } -.ck-content hr { - margin: 5px 0; - height: 1px; - background-color: var(--main-border-color); +:root .ck-content hr { + margin-block: 5px; + height: 0; + border: thin solid var(--main-border-color); opacity: 1; } diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 1ac2f13e77..195accc289 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -372,10 +372,6 @@ body[dir=ltr] #launcher-container { .calendar-dropdown-widget .calendar-header [data-calendar-input="month"] { --input-background-color: transparent; --menu-background-color: transparent; - - text-align: center; - font-size: 1.4em; - font-weight: 300; } .calendar-dropdown-widget .calendar-header input:not(:focus) { @@ -425,8 +421,6 @@ body[dir=ltr] #launcher-container { } .calendar-dropdown-widget .calendar-week span { - font-size: 0.85em; - font-weight: 500; color: var(--calendar-weekday-labels-color); } @@ -689,9 +683,10 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu { padding-inline-start: 12px; } -#left-pane span.fancytree-node.fancytree-active { +#left-pane span.fancytree-node.fancytree-active, +#left-pane span.fancytree-node.fancytree-active:hover { position: relative; - background: transparent !important; + background: transparent; color: var(--custom-color, var(--left-pane-item-selected-color)); } @@ -704,6 +699,14 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu { } } +/* + * .fancytree-node pseudo-elements: + * + * - ::before: the active tree item decorator. + * - ::after: the selected tree item background. A pseudo-element is used instead of the + * element's background color, to allow alpha compositing for the hover state. + */ + #left-pane span.fancytree-node.fancytree-active::before { position: absolute; content: ""; @@ -718,6 +721,24 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu { z-index: -1; } +#left-pane span.fancytree-node.fancytree-selected { + --left-pane-item-selected-shadow-size: 4px; + + position: relative; + background-color: transparent; + border-radius: 0; +} + +#left-pane span.fancytree-node.fancytree-selected::after { + display: block; + position: absolute; + z-index: -2; + content: ""; + inset: 0; + background: var(--selection-background-color); + animation: left-pane-item-select 100ms ease-out; +} + #left-pane span.fancytree-node.protected > span.fancytree-custom-icon { position: relative; filter: unset !important; @@ -780,7 +801,8 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i opacity: 0.5; } -#left-pane .tree-item-button { +#left-pane .tree-item-button, +#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon { margin-inline-end: 6px; border: unset; border-radius: 50%; @@ -791,7 +813,8 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i box-shadow 200ms ease-out; } -#left-pane .tree-item-button:hover { +#left-pane .tree-item-button:hover, +#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon:hover { background: var(--left-pane-item-action-button-hover-background); box-shadow: var(--left-pane-item-action-button-hover-shadow); transition: @@ -799,10 +822,41 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i box-shadow 100ms ease-in; } -#left-pane span.fancytree-node.fancytree-active .tree-item-button:hover { +#left-pane span.fancytree-node.fancytree-active .tree-item-button:hover, +#left-pane span.fancytree-node.fancytree-active.fancytree-selected .fancytree-custom-icon:hover { box-shadow: var(--left-pane-item-selected-action-button-hover-shadow); } +/* Selected item bulk action button */ + +@keyframes bulk-action-button-blink { + from { + opacity: 1; + } + to { + opacity: .3; + } +} + +#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon { + margin: 0; +} + +#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon::before { + border: 0; + font-size: .65em; +} + +#left-pane span.fancytree-node.fancytree-selected:hover .fancytree-custom-icon:not(:hover)::before { + animation: bulk-action-button-blink 500ms linear infinite alternate; +} + +#left-pane span.fancytree-node.fancytree-selected.protected .fancytree-custom-icon::after { + /* Protected note indicator */ + display: none; +} + + #context-menu-container { /* The context menu of the tree */ --menu-item-icon-vert-offset: -1px; @@ -1033,7 +1087,7 @@ body.layout-vertical.electron.platform-darwin .tab-row-container { height: var(--tab-height) !important; } -.tab-row-widget > * { +body.layout-vertical .tab-row-widget > * { margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2); } diff --git a/apps/client/src/stylesheets/tree.css b/apps/client/src/stylesheets/tree.css index 8ed03215b1..a791f0578b 100644 --- a/apps/client/src/stylesheets/tree.css +++ b/apps/client/src/stylesheets/tree.css @@ -140,10 +140,22 @@ ul.fancytree-container { background-color: inherit; } + .fancytree-custom-icon { + display: flex; + justify-content: center; + align-items: center; + width: 1em; + height: 1em; font-size: 1.2em; } +/* Fallback icon */ +:where(.fancytree-custom-icon)::before { + content: "?"; +} + +/* Protected note icon badge */ span.fancytree-node.protected > span.fancytree-custom-icon { filter: drop-shadow(2px 2px 2px var(--main-text-color)); } @@ -185,7 +197,7 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit span.fancytree-active { color: var(--active-item-text-color); - background-color: var(--active-item-background-color) !important; + background-color: var(--active-item-background-color); border-color: transparent; /* invisible border */ border-radius: 5px; } @@ -195,20 +207,15 @@ span.fancytree-active .fancytree-title { border: 0; } -span.fancytree-selected { - border-color: var(--main-border-color) !important; - border-radius: 5px; -} - -span.fancytree-selected .fancytree-title { - text-decoration: underline; - font-style: italic; +span.fancytree-node.fancytree-selected { + background-color: var(--selection-background-color); + border-radius: 0; } span.fancytree-selected .fancytree-custom-icon::before { font-family: "boxicons"; - content: "\eb43"; - border: 1px solid var(--main-border-color); + content: "\ef05"; + border: 1px solid var(--main-text-color); border-radius: 3px; } diff --git a/apps/client/src/translations/ar/translation.json b/apps/client/src/translations/ar/translation.json index 3eaf9a8432..18831c4a8a 100644 --- a/apps/client/src/translations/ar/translation.json +++ b/apps/client/src/translations/ar/translation.json @@ -882,6 +882,7 @@ "electron_context_menu": { "cut": "قص", "copy": "نسخ", + "copy-as-markdown": "نسخ كـ Markdown", "paste": "لصق", "copy-link": "نسخ الرابط", "add-term-to-dictionary": "اضافة \"{{term}}\" الى القاموس", diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 253d277679..3648e48842 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1760,6 +1760,7 @@ "add-term-to-dictionary": "将 \"{{term}}\" 添加到字典", "cut": "剪切", "copy": "复制", + "copy-as-markdown": "复制为 Markdown", "copy-link": "复制链接", "paste": "粘贴", "paste-as-plain-text": "以纯文本粘贴", diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index b90f5f8ebf..9f8bf82b0b 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -1729,6 +1729,7 @@ "add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen", "cut": "Ausschneiden", "copy": "Kopieren", + "copy-as-markdown": "Als Markdown kopieren", "copy-link": "Link kopieren", "paste": "Einfügen", "paste-as-plain-text": "Als unformatierten Text einfügen", diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 04a372ddae..4bb4c11dd6 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1810,6 +1810,7 @@ "add-term-to-dictionary": "Add \"{{term}}\" to dictionary", "cut": "Cut", "copy": "Copy", + "copy-as-markdown": "Copy as Markdown", "copy-link": "Copy link", "paste": "Paste", "paste-as-plain-text": "Paste as plain text", diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 7f4538c6d5..2c31f84f2f 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -1778,6 +1778,7 @@ "add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario", "cut": "Cortar", "copy": "Copiar", + "copy-as-markdown": "Copiar como markdown", "copy-link": "Copiar enlace", "paste": "Pegar", "paste-as-plain-text": "Pegar como texto plano", diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index c1d844ae99..be1265054b 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -1689,6 +1689,7 @@ "add-term-to-dictionary": "Ajouter «{{term}}» au dictionnaire", "cut": "Couper", "copy": "Copier", + "copy-as-markdown": "Copier en markdown", "copy-link": "Copier le lien", "paste": "Coller", "paste-as-plain-text": "Coller comme texte brut", diff --git a/apps/client/src/translations/ga/translation.json b/apps/client/src/translations/ga/translation.json index 25b5acaffd..5f8b7f8f0c 100644 --- a/apps/client/src/translations/ga/translation.json +++ b/apps/client/src/translations/ga/translation.json @@ -1808,6 +1808,7 @@ "add-term-to-dictionary": "Cuir \"{{term}}\" leis an bhfoclóir", "cut": "Gearr", "copy": "Cóipeáil", + "copy-as-markdown": "Cóipeáil mar markdown", "copy-link": "Cóipeáil nasc", "paste": "Greamaigh", "paste-as-plain-text": "Greamaigh mar théacs simplí", diff --git a/apps/client/src/translations/hi/translation.json b/apps/client/src/translations/hi/translation.json index ac5ed8073d..a48c11c6de 100644 --- a/apps/client/src/translations/hi/translation.json +++ b/apps/client/src/translations/hi/translation.json @@ -1810,6 +1810,7 @@ "add-term-to-dictionary": "\"{{term}}\" को डिक्शनरी में जोड़ें", "cut": "कट (Cut)", "copy": "कॉपी (Copy)", + "copy-as-markdown": "Markdown के रूप में कॉपी करें", "copy-link": "लिंक कॉपी करें", "paste": "पेस्ट (Paste)", "paste-as-plain-text": "प्लेन टेक्स्ट की तरह पेस्ट करें", diff --git a/apps/client/src/translations/it/translation.json b/apps/client/src/translations/it/translation.json index b4f9792b1b..c1ed534487 100644 --- a/apps/client/src/translations/it/translation.json +++ b/apps/client/src/translations/it/translation.json @@ -334,6 +334,7 @@ "electron_context_menu": { "cut": "Taglia", "copy": "Copia", + "copy-as-markdown": "Copia come markdown", "paste": "Incolla", "copy-link": "Copia collegamento", "paste-as-plain-text": "Incolla come testo semplice", @@ -519,7 +520,7 @@ "custom_name_label": "Nome del motore di ricerca personalizzato", "custom_name_placeholder": "Personalizza il nome del motore di ricerca", "custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.", - "custom_url_placeholder": "Personalizza l'URL del motore di ricerca" +"custom_url_placeholder": "Personalizza l'URL del motore di ricerca" }, "sql_table_schemas": { "tables": "Tabelle" @@ -592,7 +593,7 @@ "collapseExpand": "collassa/espande il nodo", "notSet": "non impostato", "goBackForwards": "indietro/avanti nella cronologia", - "showJumpToNoteDialog": "mostra \"Vai a\"", + "showJumpToNoteDialog": "mostra finestra \"Vai a\"", "title": "Scheda riassuntiva", "noteNavigation": "Nota navigazione", "scrollToActiveNote": "scorri fino alla nota attiva", diff --git a/apps/client/src/translations/ja/translation.json b/apps/client/src/translations/ja/translation.json index 40cca8bdc9..386712af3b 100644 --- a/apps/client/src/translations/ja/translation.json +++ b/apps/client/src/translations/ja/translation.json @@ -1353,6 +1353,7 @@ "add-term-to-dictionary": "辞書に \"{{term}}\" を追加", "cut": "切り取り", "copy": "コピー", + "copy-as-markdown": "Markdownとしてコピー", "copy-link": "リンクをコピー", "paste": "貼り付け", "paste-as-plain-text": "プレーンテキストで貼り付け", diff --git a/apps/client/src/translations/pl/translation.json b/apps/client/src/translations/pl/translation.json index 982cb71c43..41ffdb60ae 100644 --- a/apps/client/src/translations/pl/translation.json +++ b/apps/client/src/translations/pl/translation.json @@ -261,7 +261,9 @@ "percentage": "%" }, "pagination": { - "total_notes": "{{count}} notatek" + "total_notes": "{{count}} notatek", + "prev_page": "Poprzednia strona", + "next_page": "Następna strona" }, "collections": { "rendering_error": "Nie można wyświetlić zawartości z powodu błędu." @@ -612,6 +614,7 @@ "electron_context_menu": { "cut": "Wytnij", "copy": "Kopiuj", + "copy-as-markdown": "Kopiuj jako markdown", "copy-link": "Kopiuj link", "paste": "Wklej", "paste-as-plain-text": "Wklej jako zwykły tekst", @@ -737,7 +740,8 @@ "raster": "Raster", "vector_light": "Wektor (Jasny)", "vector_dark": "Wektor (Ciemny)", - "show-scale": "Pokaż skalę" + "show-scale": "Pokaż skalę", + "show-labels": "Pokaż nazwy znaczników" }, "table_context_menu": { "delete_row": "Usuń wiersz" @@ -1232,7 +1236,7 @@ "no_attachments": "Ta notatka nie ma załączników." }, "book": { - "no_children_help": "Ta kolekcja nie posiada żadnych notatek podrzędnych, więc nie ma nic do wyświetlenia. Zobacz wiki po szczegóły.", + "no_children_help": "Ta kolekcja nie zawiera notatek podrzędnych, więc nie ma nic do wyświetlenia.", "drag_locked_title": "Zablokowane do edycji", "drag_locked_message": "Przeciąganie niedozwolone, ponieważ kolekcja jest zablokowana do edycji." }, @@ -1653,7 +1657,8 @@ "description": "Opis", "reload_app": "Przeładuj aplikację, aby zastosować zmiany", "set_all_to_default": "Ustaw wszystkie skróty na domyślne", - "confirm_reset": "Czy na pewno chcesz zresetować wszystkie skróty klawiszowe do domyślnych?" + "confirm_reset": "Czy na pewno chcesz zresetować wszystkie skróty klawiszowe do domyślnych?", + "no_results": "Nie znaleziono skrótów pasujących do '{{filter}}'" }, "spellcheck": { "title": "Sprawdzanie pisowni", @@ -1860,7 +1865,9 @@ "print_report_collection_content_few": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.", "print_report_collection_content_many": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.", "print_report_collection_details_button": "Zobacz szczegóły", - "print_report_collection_details_ignored_notes": "Zignorowane notatki" + "print_report_collection_details_ignored_notes": "Zignorowane notatki", + "print_report_error_title": "Nie udało się wydrukować", + "print_report_stack_trace": "Ślad stosu" }, "note_title": { "placeholder": "wpisz tytuł notatki tutaj...", @@ -1875,7 +1882,8 @@ }, "search_result": { "no_notes_found": "Nie znaleziono notatek dla podanych parametrów wyszukiwania.", - "search_not_executed": "Wyszukiwanie nie zostało jeszcze wykonane. Kliknij przycisk \"Szukaj\" powyżej, aby zobaczyć wyniki." + "search_not_executed": "Nie przeprowadzono jeszcze wyszukiwania.", + "search_now": "Szukaj teraz" }, "spacer": { "configure_launchbar": "Konfiguruj pasek szybkiego dostępu" @@ -2145,5 +2153,49 @@ }, "bookmark_buttons": { "bookmarks": "Zakładki" + }, + "render": { + "setup_title": "Wyświetl własny kod HTML lub Preact JSX w tej notatce", + "setup_create_sample_preact": "Stwórz przykładową notatkę z użyciem Preact", + "setup_create_sample_html": "Stwórz przykładową notatkę z użyciem HTML", + "setup_sample_created": "Utworzono przykładową notatkę jako notatkę podrzędną.", + "disabled_description": "Ta notatka pochodzi z zewnętrznego źródła. Ze względów bezpieczeństwa funkcja ta nie jest domyślnie włączona. Upewnij się, że ufasz źródłu, zanim ją aktywujesz.", + "disabled_button_enable": "Włącz renderowanie notatki" + }, + "web_view_setup": { + "title": "Utwórz podgląd strony na żywo bezpośrednio w Trilium", + "url_placeholder": "Wpisz lub wklej adres strony internetowej, na przykład https://triliumnotes.org", + "create_button": "Utwórz widok strony", + "invalid_url_title": "Nieprawidłowy adres", + "invalid_url_message": "Wprowadź prawidłowy adres strony, na przykład https://triliumnotes.org.", + "disabled_description": "Ten widok strony został zaimportowany z zewnętrznego źródła. Aby chronić Cię przed phishingiem lub szkodliwą zawartością, nie jest on ładowany automatycznie. Możesz go włączyć, jeśli ufasz źródłu.", + "disabled_button_enable": "Włącz widok strony" + }, + "active_content_badges": { + "type_icon_pack": "Pakiet ikon", + "type_backend_script": "Skrypt po stronie serwera", + "type_frontend_script": "Skrypt po stronie klienta", + "type_widget": "Widżet", + "type_app_css": "Niestandardowy CSS", + "type_render_note": "Renderuj notatkę", + "type_web_view": "Widok strony", + "type_app_theme": "Własny motyw", + "toggle_tooltip_enable_tooltip": "Kliknij, aby włączyć {{type}}.", + "toggle_tooltip_disable_tooltip": "Kliknij, aby wyłączyć {{type}}.", + "menu_docs": "Otwórz dokumentację", + "menu_execute_now": "Uruchom skrypt teraz", + "menu_run": "Uruchamiaj automatycznie", + "menu_run_disabled": "Ręcznie", + "menu_run_backend_startup": "Podczas uruchamiania backendu", + "menu_run_hourly": "Co godzinę", + "menu_run_daily": "Codziennie", + "menu_run_frontend_startup": "Podczas uruchamiania desktopowego frontendu", + "menu_run_mobile_startup": "Podczas uruchamiania mobilnego frontendu", + "menu_change_to_widget": "Zmień na widżet", + "menu_change_to_frontend_script": "Zmień na skrypt frontendowy", + "menu_theme_base": "Baza motywu" + }, + "setup_form": { + "more_info": "Dowiedz się więcej" } } diff --git a/apps/client/src/translations/pt/translation.json b/apps/client/src/translations/pt/translation.json index 167d85e8c0..45570c6db4 100644 --- a/apps/client/src/translations/pt/translation.json +++ b/apps/client/src/translations/pt/translation.json @@ -1774,6 +1774,7 @@ "add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário", "cut": "Cortar", "copy": "Copiar", + "copy-as-markdown": "Copiar como markdown", "copy-link": "Copiar ligação", "paste": "Colar", "paste-as-plain-text": "Colar como texto sem formatação", diff --git a/apps/client/src/translations/pt_br/translation.json b/apps/client/src/translations/pt_br/translation.json index 3db8d8c8e1..22fd6856d1 100644 --- a/apps/client/src/translations/pt_br/translation.json +++ b/apps/client/src/translations/pt_br/translation.json @@ -1637,6 +1637,7 @@ "add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário", "cut": "Cortar", "copy": "Copiar", + "copy-as-markdown": "Copiar como markdown", "copy-link": "Copiar link", "paste": "Colar", "paste-as-plain-text": "Colar como texto sem formatação", diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index a7bfd1f00b..e1f5810cf0 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -1724,6 +1724,7 @@ "electron_context_menu": { "add-term-to-dictionary": "Adaugă „{{term}}” în dicționar", "copy": "Copiază", + "copy-as-markdown": "Copiază ca markdown", "copy-link": "Copiază legătura", "cut": "Decupează", "paste": "Lipește", diff --git a/apps/client/src/translations/ru/translation.json b/apps/client/src/translations/ru/translation.json index f023814bdc..c3e1344e2d 100644 --- a/apps/client/src/translations/ru/translation.json +++ b/apps/client/src/translations/ru/translation.json @@ -708,6 +708,7 @@ "paste": "Вставить", "copy-link": "Скопировать ссылку", "copy": "Скопировать", + "copy-as-markdown": "Копировать как Markdown", "cut": "Вырезать", "search_online": "Поиск \"{{term}}\" в {{searchEngine}}", "add-term-to-dictionary": "Добавить \"{{term}}\" в словарь", diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index 71eaf097a5..9eecb868e1 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -1722,6 +1722,7 @@ "add-term-to-dictionary": "將 \"{{term}}\" 新增至字典", "cut": "剪下", "copy": "複製", + "copy-as-markdown": "複製為 Markdown", "copy-link": "複製連結", "paste": "貼上", "paste-as-plain-text": "以純文字貼上", diff --git a/apps/client/src/translations/uk/translation.json b/apps/client/src/translations/uk/translation.json index 8861fca0bc..6c1951a035 100644 --- a/apps/client/src/translations/uk/translation.json +++ b/apps/client/src/translations/uk/translation.json @@ -1533,6 +1533,7 @@ "add-term-to-dictionary": "Додати \"{{term}}\" до словника", "cut": "Вирізати", "copy": "Копіювати", + "copy-as-markdown": "Копіювати як Markdown", "copy-link": "Копіювати посилання", "paste": "Вставити", "paste-as-plain-text": "Вставити як звичайний текст", diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css index 4851410db8..73bd997a6e 100644 --- a/apps/client/src/widgets/collections/board/index.css +++ b/apps/client/src/widgets/collections/board/index.css @@ -14,7 +14,7 @@ height: 100%; display: flex; gap: 1em; - padding-inline: 12px; + margin-inline: var(--content-margin-inline); padding-block: 4px; align-items: flex-start; overflow-x: auto; diff --git a/apps/client/src/widgets/collections/calendar/event_builder.spec.ts b/apps/client/src/widgets/collections/calendar/event_builder.spec.ts index 2c872a14e5..88299e8b65 100644 --- a/apps/client/src/widgets/collections/calendar/event_builder.spec.ts +++ b/apps/client/src/widgets/collections/calendar/event_builder.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { buildNote, buildNotes } from "../../../test/easy-froca.js"; import { buildEvent, buildEvents } from "./event_builder.js"; import { LOCALE_MAPPINGS } from "./index.js"; @@ -148,7 +148,7 @@ describe("Promoted attributes", () => { expect(event).toHaveLength(1); expect(event[0]?.promotedAttributes).toMatchObject([ [ "assignee", "Target note" ] - ]) + ]); }); it("supports start time and end time", async () => { @@ -177,6 +177,86 @@ describe("Promoted attributes", () => { }); + +describe("Recurrence", () => { + it("supports valid recurrence without end date", async () => { + const noteIds = buildNotes([ + { + title: "Recurring Event", + "#startDate": "2025-05-05", + "#recurrence": "FREQ=DAILY;COUNT=5" + } + ]); + const events = await buildEvents(noteIds); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + title: "Recurring Event", + start: "2025-05-05", + }); + expect(events[0].rrule).toContain("DTSTART:20250505"); + expect(events[0].rrule).toContain("FREQ=DAILY;COUNT=5"); + expect(events[0].end).toBeUndefined(); + }); + + it("supports recurrence with start and end time (duration calculated)", async () => { + const noteIds = buildNotes([ + { + title: "Timed Recurring Event", + "#startDate": "2025-05-05", + "#startTime": "13:00", + "#endTime": "15:30", + "#recurrence": "FREQ=WEEKLY;COUNT=3" + } + ]); + const events = await buildEvents(noteIds); + + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + title: "Timed Recurring Event", + start: "2025-05-05T13:00:00", + duration: "02:30" + }); + expect(events[0].rrule).toContain("DTSTART:20250505T130000"); + expect(events[0].end).toBeUndefined(); + }); + + it("removes end date when recurrence is valid", async () => { + const noteIds = buildNotes([ + { + title: "Recurring With End", + "#startDate": "2025-05-05", + "#endDate": "2025-05-07", + "#recurrence": "FREQ=DAILY;COUNT=2" + } + ]); + const events = await buildEvents(noteIds); + + expect(events).toHaveLength(1); + expect(events[0].rrule).toBeDefined(); + expect(events[0].end).toBeUndefined(); + }); + + it("writes to console on invalid recurrence rule", async () => { + const noteIds = buildNotes([ + { + title: "Invalid Recurrence", + "#startDate": "2025-05-05", + "#recurrence": "RRULE:FREQ=INVALID" + } + ]); + + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + await buildEvents(noteIds); + const calledWithInvalid = consoleSpy.mock.calls.some(call => + call[0].includes("has an invalid #recurrence string") + ); + expect(calledWithInvalid).toBe(true); + consoleSpy.mockRestore(); + }); +}); + + describe("Building locales", () => { it("every language has a locale defined", async () => { for (const { id, contentOnly } of LOCALES) { diff --git a/apps/client/src/widgets/collections/calendar/event_builder.ts b/apps/client/src/widgets/collections/calendar/event_builder.ts index f4611ccd7e..dec64feee8 100644 --- a/apps/client/src/widgets/collections/calendar/event_builder.ts +++ b/apps/client/src/widgets/collections/calendar/event_builder.ts @@ -1,17 +1,22 @@ import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js"; +import { dayjs } from "@triliumnext/commons"; import clsx from "clsx"; +import { start } from "repl"; +import * as rruleLib from 'rrule'; import FNote from "../../../entities/fnote"; import froca from "../../../services/froca"; import server from "../../../services/server"; -import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils"; +import toastService from "../../../services/toast"; +import { getCustomisableLabel, getMonthsInDateRange } from "./utils"; interface Event { startDate: string, endDate?: string | null, startTime?: string | null, endTime?: string | null, - isArchived?: boolean; + isArchived?: boolean, + recurrence?: string | null; } export async function buildEvents(noteIds: string[]) { @@ -28,8 +33,17 @@ export async function buildEvents(noteIds: string[]) { const endDate = getCustomisableLabel(note, "endDate", "calendar:endDate"); const startTime = getCustomisableLabel(note, "startTime", "calendar:startTime"); const endTime = getCustomisableLabel(note, "endTime", "calendar:endTime"); + const recurrence = getCustomisableLabel(note, "recurrence", "calendar:recurrence"); const isArchived = note.hasLabel("archived"); - events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, isArchived })); + try { + events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, recurrence, isArchived })); + } catch (error) { + if (error instanceof Error) { + const errorMessage = error.message; + toastService.showError(errorMessage); + console.error(errorMessage); + } + } } return events.flat(); @@ -59,6 +73,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg) events.push(await buildEvent(dateNote, { startDate })); + if (dateNote.hasChildren()) { const childNoteIds = await dateNote.getSubtreeNoteIds(); for (const childNoteId of childNoteIds) { @@ -79,7 +94,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg) return events.flat(); } -export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) { +export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, recurrence, isArchived }: Event) { const customTitleAttributeName = note.getLabelValue("calendar:title"); const titles = await parseCustomTitle(customTitleAttributeName, note); const colorClass = note.getColorClass(); @@ -98,9 +113,10 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e startDate = (startTime ? `${startDate}T${startTime}:00` : startDate); if (!startTime) { - const endDateOffset = offsetDate(endDate ?? startDate, 1); - if (endDateOffset) { - endDate = formatDateToLocalISO(endDateOffset); + if (endDate) { + endDate = dayjs(endDate).add(1, "day").format("YYYY-MM-DD"); + } else if (startDate) { + endDate = dayjs(startDate).add(1, "day").format("YYYY-MM-DD"); } } @@ -118,6 +134,30 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e if (endDate) { eventData.end = endDate; } + + if (recurrence) { + // Generate rrule string + const rruleString = `DTSTART:${dayjs(startDate).format("YYYYMMDD[T]HHmmss")}\n${recurrence}`; + + // Validate rrule string + let rruleValid = true; + try { + rruleLib.rrulestr(rruleString, { forceset: true }) as rruleLib.RRuleSet; + } catch { + rruleValid = false; + } + + if (rruleValid) { + delete eventData.end; + eventData.rrule = rruleString; + if (endDate){ + const duration = dayjs.duration(dayjs(endDate).diff(dayjs(startDate))); + eventData.duration = duration.format("HH:mm"); + } + } else { + throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string ${recurrence}. Excluding...`); + } + } events.push(eventData); } return events; diff --git a/apps/client/src/widgets/collections/calendar/index.css b/apps/client/src/widgets/collections/calendar/index.css index 34296dc174..df93910b50 100644 --- a/apps/client/src/widgets/collections/calendar/index.css +++ b/apps/client/src/widgets/collections/calendar/index.css @@ -52,7 +52,7 @@ --fc-border-color: var(--main-border-color); --fc-neutral-bg-color: var(--launcher-pane-background-color); --fc-list-event-hover-bg-color: var(--left-pane-item-hover-background); - padding: 0 12px; + padding: 0 var(--content-margin-inline); } .calendar-container .fc-list-sticky .fc-list-day > * { diff --git a/apps/client/src/widgets/collections/calendar/index.tsx b/apps/client/src/widgets/collections/calendar/index.tsx index 23f8371125..349d666e6c 100644 --- a/apps/client/src/widgets/collections/calendar/index.tsx +++ b/apps/client/src/widgets/collections/calendar/index.tsx @@ -252,6 +252,7 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) { plugins.push((await import("@fullcalendar/timegrid")).default); plugins.push((await import("@fullcalendar/list")).default); plugins.push((await import("@fullcalendar/multimonth")).default); + plugins.push((await import("@fullcalendar/rrule")).default); if (isEditable || isCalendarRoot) { plugins.push((await import("@fullcalendar/interaction")).default); } diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.css b/apps/client/src/widgets/collections/legacy/ListOrGridView.css index 10daa7e722..4dde39124a 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.css +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.css @@ -234,7 +234,7 @@ } &.type-pdf { - iframe { + div { height: 50vh; } diff --git a/apps/client/src/widgets/collections/table/index.css b/apps/client/src/widgets/collections/table/index.css index 5d0725250c..604a384156 100644 --- a/apps/client/src/widgets/collections/table/index.css +++ b/apps/client/src/widgets/collections/table/index.css @@ -13,6 +13,7 @@ .table-view-container { height: 100%; + margin-inline-start: var(--content-margin-inline); } .search-result-widget-content .table-view { diff --git a/apps/client/src/widgets/launch_bar/CalendarWidget.css b/apps/client/src/widgets/launch_bar/CalendarWidget.css index a47d026278..d6b9d22645 100644 --- a/apps/client/src/widgets/launch_bar/CalendarWidget.css +++ b/apps/client/src/widgets/launch_bar/CalendarWidget.css @@ -35,7 +35,7 @@ .calendar-dropdown-widget .calendar-header { align-items: center; display: flex; - padding: 0 0.5rem 0.5rem 0.5rem; + padding: 0 0.5rem 1rem 0.5rem; } .calendar-dropdown-widget .calendar-header>div { @@ -65,8 +65,13 @@ border: 0; border-inline-start: unset; background-color: var(--menu-background-color); - font-weight: bold; outline: 0; + font-weight: 300; + font-size: 1.4em; +} + +.calendar-dropdown-widget .calendar-header .dropdown-toggle { + justify-content: center; } .calendar-dropdown-widget .calendar-header .dropdown-toggle::after { @@ -82,18 +87,20 @@ .calendar-dropdown-widget .calendar-week span { flex-direction: column; flex: 0 0 12.5%; - font-size: 1rem; - font-weight: bold; max-width: 12.5%; padding-top: 5px; padding-bottom: 5px; text-align: center; text-transform: uppercase; + font-size: .85em; + font-weight: 500; } .calendar-dropdown-widget .calendar-body { display: flex; flex-wrap: wrap; + min-height: 250px; + align-items: center; } .calendar-dropdown-widget .calendar-week-number { diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index 4ae4c15816..9301bf5012 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -4,10 +4,11 @@ body.experimental-feature-new-layout { } .title-actions { - --title-actions-padding-start: 12px; - --title-actions-padding-end: 8px; + --title-actions-padding-start: var(--content-margin-inline); + --title-actions-padding-end: var(--content-margin-inline); display: flex; + width: 100%; max-width: var(--max-content-width); flex-direction: column; gap: 0.5em; diff --git a/apps/client/src/widgets/layout/NoteTypeSwitcher.css b/apps/client/src/widgets/layout/NoteTypeSwitcher.css index 574924d037..495d055d32 100644 --- a/apps/client/src/widgets/layout/NoteTypeSwitcher.css +++ b/apps/client/src/widgets/layout/NoteTypeSwitcher.css @@ -11,7 +11,6 @@ position: relative; top: 5px; - padding: .25em 0; display: flex; align-items: center; overflow-x: auto; diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.css b/apps/client/src/widgets/ribbon/NotePathsTab.css index 64c7374480..201117cce9 100644 --- a/apps/client/src/widgets/ribbon/NotePathsTab.css +++ b/apps/client/src/widgets/ribbon/NotePathsTab.css @@ -17,11 +17,13 @@ body.experimental-feature-new-layout .note-paths-widget { padding: 8px 20px 8px 25px; &:first-child { - border-radius: var(--border-radius) var(--border-radius) 0 0; + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); } &:last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); } & + li { diff --git a/apps/client/src/widgets/search_result.css b/apps/client/src/widgets/search_result.css index 13acf04e9d..8888bd01ac 100644 --- a/apps/client/src/widgets/search_result.css +++ b/apps/client/src/widgets/search_result.css @@ -4,8 +4,8 @@ contain: none !important; } -.search-result-widget .note-list { - padding: 10px; +.search-result-widget .note-list-wrapper { + margin-inline: var(--content-margin-inline); } .note-split.type-search .scrolling-container { diff --git a/apps/client/src/widgets/type_widgets/ProtectedSession.css b/apps/client/src/widgets/type_widgets/ProtectedSession.css index 6bfcb87737..41a24b8d65 100644 --- a/apps/client/src/widgets/type_widgets/ProtectedSession.css +++ b/apps/client/src/widgets/type_widgets/ProtectedSession.css @@ -3,4 +3,8 @@ margin-inline: 40px; flex-direction: column; justify-content: center; + + label { + margin-bottom: 8px; + } } \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/file/PdfViewer.tsx b/apps/client/src/widgets/type_widgets/file/PdfViewer.tsx index 7e6870dd42..6415b89dd6 100644 --- a/apps/client/src/widgets/type_widgets/file/PdfViewer.tsx +++ b/apps/client/src/widgets/type_widgets/file/PdfViewer.tsx @@ -1,14 +1,16 @@ import type { HTMLAttributes, RefObject } from "preact"; import { useCallback, useEffect, useRef } from "preact/hooks"; - +import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf"; import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; -const VARIABLE_WHITELIST = new Set([ - "root-background", - "main-background-color", - "main-border-color", - "main-text-color" -]); +interface FontDefinition { + name: string; + url: string; +} + +const FONTS: FontDefinition[] = [ + {name: "Inter", url: Inter}, +] interface PdfViewerProps extends Pick, "tabIndex"> { iframeRef?: RefObject; @@ -34,6 +36,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad