diff --git a/.github/actions/report-size/action.yml b/.github/actions/report-size/action.yml index f83206a696..372a6b9ec3 100644 --- a/.github/actions/report-size/action.yml +++ b/.github/actions/report-size/action.yml @@ -69,7 +69,7 @@ runs: # Post github action comment - name: Post comment - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 if: ${{ steps.bundleSize.outputs.hasDifferences == 'true' }} # post only in case of changes with: number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index c31af9d701..e55828e090 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -41,7 +41,7 @@ jobs: run: pnpm run --filter=client test - name: Upload client test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: client-test-report @@ -52,7 +52,7 @@ jobs: run: pnpm run --filter=server test - name: Upload server test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 if: always() with: name: server-test-report @@ -89,7 +89,7 @@ jobs: key: ${{ secrets.RELATIVE_CI_CLIENT_KEY }} - name: Trigger server build run: pnpm run server:build - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-buildx-action@v4 - uses: docker/build-push-action@v7 with: context: apps/server @@ -124,7 +124,7 @@ jobs: run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and export to Docker uses: docker/build-push-action@v7 diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index cab55d9399..7c00d1a5e0 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -40,7 +40,7 @@ jobs: run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - uses: pnpm/action-setup@v4 - name: Set up node & dependencies @@ -178,7 +178,7 @@ jobs: uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to GHCR uses: docker/login-action@v4 @@ -229,7 +229,7 @@ jobs: run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV - name: Set up crane - uses: imjasonh/setup-crane@v0.4 + uses: imjasonh/setup-crane@v0.5 - name: Login to GHCR uses: docker/login-action@v4 diff --git a/.gitignore b/.gitignore index 44d9b523dc..994679ebe5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ upload /.direnv /result -.svelte-kit # docs site/ diff --git a/apps/build-docs/package.json b/apps/build-docs/package.json index 3ed9ec91a7..27adb34015 100644 --- a/apps/build-docs/package.json +++ b/apps/build-docs/package.json @@ -14,9 +14,9 @@ "keywords": [], "author": "Elian Doran ", "license": "AGPL-3.0-only", - "packageManager": "pnpm@10.30.3", + "packageManager": "pnpm@10.32.1", "devDependencies": { - "@redocly/cli": "2.20.2", + "@redocly/cli": "2.21.1", "archiver": "7.0.1", "fs-extra": "11.3.4", "js-yaml": "4.1.1", diff --git a/apps/client/package.json b/apps/client/package.json index d4e7781954..df03795948 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/client", - "version": "0.102.0", + "version": "0.102.1", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", @@ -25,19 +25,25 @@ "@fullcalendar/rrule": "6.1.20", "@fullcalendar/timegrid": "6.1.20", "@maplibre/maplibre-gl-leaflet": "0.1.3", - "@mermaid-js/layout-elk": "0.2.0", + "@mermaid-js/layout-elk": "0.2.1", "@mind-elixir/node-menu": "5.0.1", "@popperjs/core": "2.11.8", - "@preact/signals": "2.8.1", + "@preact/signals": "2.8.2", "@triliumnext/ckeditor5": "workspace:*", "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "@triliumnext/split.js": "workspace:*", - "@univerjs/preset-sheets-core": "0.16.1", - "@univerjs/presets": "0.16.1", - "@zumer/snapdom": "2.0.2", + "@univerjs/preset-sheets-conditional-formatting": "0.17.0", + "@univerjs/preset-sheets-core": "0.17.0", + "@univerjs/preset-sheets-data-validation": "0.17.0", + "@univerjs/preset-sheets-filter": "0.17.0", + "@univerjs/preset-sheets-find-replace": "0.17.0", + "@univerjs/preset-sheets-note": "0.17.0", + "@univerjs/preset-sheets-sort": "0.17.0", + "@univerjs/presets": "0.17.0", + "@zumer/snapdom": "2.1.0", "autocomplete.js": "0.38.1", "bootstrap": "5.3.8", "boxicons": "2.1.4", @@ -45,30 +51,30 @@ "color": "5.0.3", "debounce": "3.0.0", "draggabilly": "3.0.0", - "force-graph": "1.51.1", + "force-graph": "1.51.2", "globals": "17.4.0", - "i18next": "25.8.13", + "i18next": "25.8.18", "i18next-http-backend": "3.0.2", "jquery": "4.0.0", "jquery.fancytree": "2.38.5", "jsplumb": "2.15.6", - "katex": "0.16.33", - "knockout": "3.5.1", + "katex": "0.16.38", + "knockout": "3.5.2", "leaflet": "1.9.4", "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "17.0.4", - "mermaid": "11.12.3", - "mind-elixir": "5.9.1", + "mermaid": "11.13.0", + "mind-elixir": "5.9.3", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "preact": "10.28.4", - "react-i18next": "16.5.5", + "preact": "10.29.0", + "react-i18next": "16.5.8", "react-window": "2.2.7", - "reveal.js": "5.2.1", + "reveal.js": "6.0.0", "rrule": "2.8.1", "svg-pan-zoom": "3.6.2", - "tabulator-tables": "6.3.1", + "tabulator-tables": "6.4.0", "vanilla-js-wheel-zoom": "9.0.4" }, "devDependencies": { @@ -79,12 +85,11 @@ "@types/leaflet": "1.9.21", "@types/leaflet-gpx": "1.3.8", "@types/mark.js": "8.11.12", - "@types/reveal.js": "5.2.2", "@types/tabulator-tables": "6.3.1", "copy-webpack-plugin": "14.0.0", - "happy-dom": "20.8.3", - "lightningcss": "1.31.1", + "happy-dom": "20.8.4", + "lightningcss": "1.32.0", "script-loader": "0.7.2", - "vite-plugin-static-copy": "3.2.0" + "vite-plugin-static-copy": "3.3.0" } } \ No newline at end of file diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index afcaaf0918..3ca6a2a792 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -381,6 +381,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> // Collections must always display a note list, even if no children. if (note.type === "book") { + if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { + return false; + } + const viewType = note.getLabelValue("viewType") ?? "grid"; if (!["list", "grid"].includes(viewType)) { return true; diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index afd53c53f9..ec29f094b5 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -54,7 +54,7 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo await renderText(entity, $renderedContent, options); } else if (type === "code") { await renderCode(entity, $renderedContent); - } else if (["image", "canvas", "mindMap"].includes(type)) { + } else if (["image", "canvas", "mindMap", "spreadsheet"].includes(type)) { renderImage(entity, $renderedContent, options); } else if (!options.tooltip && ["file", "pdf", "audio", "video"].includes(type)) { await renderFile(entity, type, $renderedContent); diff --git a/apps/client/src/services/froca_updater.ts b/apps/client/src/services/froca_updater.ts index 6d6ef9213d..ca6c792746 100644 --- a/apps/client/src/services/froca_updater.ts +++ b/apps/client/src/services/froca_updater.ts @@ -110,7 +110,12 @@ function processNoteChange(loadResults: LoadResults, ec: EntityChange) { } } - if (ec.componentId) { + // Only register as a content change if the protection status didn't change. + // When isProtected changes, the blobId change is a side effect of re-encryption, + // not a content edit. Registering it as content would cause the tree's content-only + // filter to incorrectly skip the note update (since both changes share the same + // componentId). + if (ec.componentId && note.isProtected === (ec.entity as FNoteRow).isProtected) { loadResults.addNoteContent(note.noteId, ec.componentId); } } diff --git a/apps/client/src/services/note_create.ts b/apps/client/src/services/note_create.ts index 00ae717d21..9c4f37f4c5 100644 --- a/apps/client/src/services/note_create.ts +++ b/apps/client/src/services/note_create.ts @@ -1,15 +1,17 @@ +import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import { AttributeRow } from "@triliumnext/commons"; + import appContext from "../components/app_context.js"; +import type FBranch from "../entities/fbranch.js"; +import type FNote from "../entities/fnote.js"; +import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; +import froca from "./froca.js"; +import { t } from "./i18n.js"; import protectedSessionHolder from "./protected_session_holder.js"; import server from "./server.js"; -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"; -import type FNote from "../entities/fnote.js"; -import type FBranch from "../entities/fbranch.js"; -import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js"; -import type { CKTextEditor } from "@triliumnext/ckeditor5"; +import treeService from "./tree.js"; +import ws from "./ws.js"; export interface CreateNoteOpts { isProtected?: boolean; @@ -24,6 +26,8 @@ export interface CreateNoteOpts { target?: string; targetBranchId?: string; textEditor?: CKTextEditor; + /** Attributes to be set on the note. These are set atomically on note creation, so entity changes are not sent for attributes defined here. */ + attributes?: Omit[]; } interface Response { @@ -37,7 +41,7 @@ interface DuplicateResponse { note: FNote; } -async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}) { +async function createNote(parentNotePath: string | undefined, options: CreateNoteOpts = {}, componentId?: string) { options = Object.assign( { activate: true, @@ -63,22 +67,15 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot const parentNoteId = treeService.getNoteIdFromUrl(parentNotePath); - if (options.type === "mermaid" && !options.content && !options.templateNoteId) { - options.content = `graph TD; - A-->B; - A-->C; - B-->D; - C-->D;`; - } - const { note, branch } = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId || ""}`, { title: options.title, content: options.content || "", isProtected: options.isProtected, type: options.type, mime: options.mime, - templateNoteId: options.templateNoteId - }); + templateNoteId: options.templateNoteId, + attributes: options.attributes + }, componentId); if (options.saveSelection) { // we remove the selection only after it was saved to server to make sure we don't lose anything @@ -140,9 +137,8 @@ function parseSelectedHtml(selectedHtml: string) { const content = selectedHtml.replace(dom[0].outerHTML, ""); return [title, content]; - } else { - return [null, selectedHtml]; } + return [null, selectedHtml]; } async function duplicateSubtree(noteId: string, parentNotePath: string) { diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index fb1e598ec2..627e28622a 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -89,7 +89,7 @@ async function remove(url: string, componentId?: string) { return await call("DELETE", url, componentId); } -async function upload(url: string, fileToUpload: File, componentId?: string) { +async function upload(url: string, fileToUpload: File, componentId?: string, method = "PUT") { const formData = new FormData(); formData.append("upload", fileToUpload); @@ -99,7 +99,7 @@ async function upload(url: string, fileToUpload: File, componentId?: string) { "trilium-component-id": componentId } : undefined), data: formData, - type: "PUT", + type: method, timeout: 60 * 60 * 1000, contentType: false, // NEEDED, DON'T REMOVE THIS processData: false // NEEDED, DON'T REMOVE THIS diff --git a/apps/client/src/setup.ts b/apps/client/src/setup.ts index c53f91f3ab..e6861c7119 100644 --- a/apps/client/src/setup.ts +++ b/apps/client/src/setup.ts @@ -158,6 +158,8 @@ function getSyncInProgress() { } addEventListener("DOMContentLoaded", (event) => { - ko.applyBindings(new SetupModel(getSyncInProgress()), document.getElementById("setup-dialog")); + const rootNode = document.getElementById("setup-dialog"); + if (!rootNode) return; + ko.applyBindings(new SetupModel(getSyncInProgress()), rootNode); $("#setup-dialog").show(); }); diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 4171a46c16..5a462b9804 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1612,11 +1612,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { } body.mobile #launcher-container { - justify-content: center; - } - - body.mobile #launcher-container button { - margin: 0 16px; + justify-content: space-evenly; } body.mobile .modal.show { diff --git a/apps/client/src/translations/ar/translation.json b/apps/client/src/translations/ar/translation.json index 18831c4a8a..8f0c6809a5 100644 --- a/apps/client/src/translations/ar/translation.json +++ b/apps/client/src/translations/ar/translation.json @@ -803,12 +803,13 @@ "web-view": "عرض الويب", "mind-map": "خريطة ذهنية", "geo-map": "خريطة جغرافية", - "task-list": "قائمة المهام" + "task-list": "قائمة المهام", + "spreadsheet": "جدول البيانات" }, "shared_switch": { "shared": "مشترك", "toggle-on-title": "مشاركة الملاحظة", - "toggle-off-title": "الغاء مشاركة الملاحظة" + "toggle-off-title": "إلغاء مشاركة الملاحظة" }, "template_switch": { "template": "قالب" @@ -1286,8 +1287,10 @@ "search-for": "بحث ل \"{{term}}\"" }, "protect_note": { - "toggle-off": "ازالة الحماية عن الملاحظة", - "toggle-on": "حماية الملاحظة" + "toggle-off": "إزالة الحماية عن الملاحظة", + "toggle-on": "حماية الملاحظة", + "toggle-on-hint": "الملاحظة غير محمة، انقر لحمايتها", + "toggle-off-hint": "الملاحظة محمية، انقر لإزالة الحماية منها" }, "open-help-page": "فتح صفحة المساعدة", "empty": { diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 3648e48842..42291a173e 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1535,7 +1535,8 @@ "new-feature": "新建", "collections": "集合", "book": "集合", - "ai-chat": "AI聊天" + "ai-chat": "AI聊天", + "spreadsheet": "电子表格" }, "protect_note": { "toggle-on": "保护笔记", diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index 9f8bf82b0b..b940edc8e9 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -1488,20 +1488,21 @@ "mermaid-diagram": "Mermaid Diagramm", "canvas": "Leinwand", "web-view": "Webansicht", - "mind-map": "Mind Map", + "mind-map": "Mindmap", "file": "Datei", "image": "Bild", "launcher": "Starter", "doc": "Dokument", "widget": "Widget", - "confirm-change": "Es is nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?", + "confirm-change": "Es ist nicht empfehlenswert den Notiz-Typ zu ändern, wenn der Inhalt der Notiz nicht leer ist. Möchtest du dennoch fortfahren?", "geo-map": "Geo-Karte", "beta-feature": "Beta", "book": "Sammlung", - "ai-chat": "KI Chat", + "ai-chat": "KI-Chat", "task-list": "Aufgabenliste", "new-feature": "Neu", - "collections": "Sammlungen" + "collections": "Sammlungen", + "spreadsheet": "Tabelle" }, "protect_note": { "toggle-on": "Notiz schützen", diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 2e228a9825..07f7b10de0 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1036,6 +1036,25 @@ "file_preview_not_available": "File preview is not available for this file format.", "too_big": "The preview only shows the first {{maxNumChars}} characters of the file for performance reasons. Download the file and open it externally to be able to see the entire content." }, + "media": { + "play": "Play (Space)", + "pause": "Pause (Space)", + "back-10s": "Back 10s (Left arrow key)", + "forward-30s": "Forward 30s", + "mute": "Mute (M)", + "unmute": "Unmute (M)", + "playback-speed": "Playback speed", + "loop": "Loop", + "disable-loop": "Disable loop", + "rotate": "Rotate", + "picture-in-picture": "Picture-in-picture", + "exit-picture-in-picture": "Exit picture-in-picture", + "fullscreen": "Fullscreen (F)", + "exit-fullscreen": "Exit fullscreen", + "unsupported-format": "Media preview is not available for this file format:\n{{mime}}", + "zoom-to-fit": "Zoom to fill", + "zoom-reset": "Reset zoom to fill" + }, "protected_session": { "enter_password_instruction": "Showing protected note requires entering your password:", "start_session_button": "Start protected session", @@ -2183,5 +2202,33 @@ }, "setup_form": { "more_info": "Learn more" + }, + "mermaid": { + "placeholder": "Type the content of your Mermaid diagram or use one of the sample diagrams below.", + "sample_diagrams": "Sample diagrams:", + "sample_flowchart": "Flowchart", + "sample_class": "Class", + "sample_sequence": "Sequence", + "sample_entity_relationship": "Entity Relationship", + "sample_state": "State", + "sample_mindmap": "Mindmap", + "sample_architecture": "Architecture", + "sample_block": "Block", + "sample_c4": "C4", + "sample_gantt": "Gantt", + "sample_git": "Git", + "sample_kanban": "Kanban", + "sample_packet": "Packet", + "sample_pie": "Pie", + "sample_quadrant": "Quadrant", + "sample_radar": "Radar", + "sample_requirement": "Requirement", + "sample_sankey": "Sankey", + "sample_timeline": "Timeline", + "sample_treemap": "Treemap", + "sample_user_journey": "User Journey", + "sample_xy": "XY", + "sample_venn": "Venn", + "sample_ishikawa": "Ishikawa" } } diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 2c31f84f2f..c2ee438b43 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -1548,7 +1548,8 @@ "task-list": "Lista de tareas", "book": "Colección", "new-feature": "Nuevo", - "collections": "Colecciones" + "collections": "Colecciones", + "spreadsheet": "Hoja de cálculo" }, "protect_note": { "toggle-on": "Proteger la nota", @@ -1650,7 +1651,8 @@ }, "search_result": { "no_notes_found": "No se han encontrado notas para los parámetros de búsqueda dados.", - "search_not_executed": "La búsqueda aún no se ha ejecutado. Dé clic en el botón «Buscar» para ver los resultados." + "search_not_executed": "La búsqueda aún no se ha ejecutado.", + "search_now": "Buscar ahora" }, "spacer": { "configure_launchbar": "Configurar barra de lanzamiento" @@ -2196,5 +2198,24 @@ }, "setup_form": { "more_info": "Para saber más" + }, + "media": { + "play": "Reproducir (Espacio)", + "pause": "Pausa (Espacio)", + "back-10s": "Retroceder 10s (tecla de flecha izquierda)", + "forward-30s": "Adelantar 30s", + "mute": "Silenciar (M)", + "unmute": "Activar sonido (M)", + "playback-speed": "Velocidad de reproducción", + "loop": "Bucle", + "disable-loop": "Deshabilitar bucle", + "rotate": "Rotar", + "picture-in-picture": "Imagen en imagen", + "exit-picture-in-picture": "Salir del modo imagen en imagen", + "fullscreen": "Pantalla completa (F)", + "exit-fullscreen": "Salir de la pantalla completa", + "unsupported-format": "La vista previa del medio no está disponible para este formato de archivo:\n{{mime}}", + "zoom-to-fit": "Acercamiento para llenar", + "zoom-reset": "Reiniciar acercamiento para llenar" } } diff --git a/apps/client/src/translations/ga/translation.json b/apps/client/src/translations/ga/translation.json index 5f8b7f8f0c..02a184591e 100644 --- a/apps/client/src/translations/ga/translation.json +++ b/apps/client/src/translations/ga/translation.json @@ -1571,7 +1571,8 @@ "ai-chat": "Comhrá AI", "task-list": "Liosta Tascanna", "new-feature": "Nua", - "collections": "Bailiúcháin" + "collections": "Bailiúcháin", + "spreadsheet": "Scarbhileog" }, "protect_note": { "toggle-on": "Cosain an nóta", diff --git a/apps/client/src/translations/it/translation.json b/apps/client/src/translations/it/translation.json index c1ed534487..e84ba2db78 100644 --- a/apps/client/src/translations/it/translation.json +++ b/apps/client/src/translations/it/translation.json @@ -520,7 +520,7 @@ "custom_name_label": "Nome del motore di ricerca personalizzato", "custom_name_placeholder": "Personalizza il nome del motore di ricerca", "custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.", -"custom_url_placeholder": "Personalizza l'URL del motore di ricerca" + "custom_url_placeholder": "Personalizza l'URL del motore di ricerca" }, "sql_table_schemas": { "tables": "Tabelle" @@ -1717,7 +1717,8 @@ "task-list": "Elenco delle attività", "new-feature": "Nuovo", "collections": "Collezioni", - "ai-chat": "Chat con IA" + "ai-chat": "Chat con IA", + "spreadsheet": "Foglio di calcolo" }, "protect_note": { "toggle-on": "Proteggi la nota", diff --git a/apps/client/src/translations/ja/translation.json b/apps/client/src/translations/ja/translation.json index 386712af3b..0cfb0616e5 100644 --- a/apps/client/src/translations/ja/translation.json +++ b/apps/client/src/translations/ja/translation.json @@ -600,7 +600,8 @@ "task-list": "タスクリスト", "new-feature": "New", "collections": "コレクション", - "ai-chat": "AI チャット" + "ai-chat": "AI チャット", + "spreadsheet": "スプレッドシート" }, "edited_notes": { "no_edited_notes_found": "この日の編集されたノートはまだありません...", @@ -2167,5 +2168,24 @@ }, "setup_form": { "more_info": "さらに詳しく" + }, + "media": { + "play": "再生 (スペース)", + "pause": "一時停止 (スペース)", + "back-10s": "10 秒戻る (左矢印キー)", + "forward-30s": "30 秒進む", + "mute": "ミュート (M)", + "unmute": "ミュート解除 (M)", + "playback-speed": "再生速度", + "loop": "ループ", + "disable-loop": "ループを解除", + "rotate": "回転", + "picture-in-picture": "ピクチャーインピクチャー", + "exit-picture-in-picture": "ピクチャーインピクチャーを終了", + "fullscreen": "全画面表示 (F)", + "exit-fullscreen": "全画面表示を終了", + "unsupported-format": "このファイル形式ではメディアプレビューはご利用いただけません:\n{{mime}}", + "zoom-to-fit": "ズームして全体を表示", + "zoom-reset": "ズーム設定をリセット" } } diff --git a/apps/client/src/translations/pl/translation.json b/apps/client/src/translations/pl/translation.json index 41ffdb60ae..885408e3d1 100644 --- a/apps/client/src/translations/pl/translation.json +++ b/apps/client/src/translations/pl/translation.json @@ -1780,7 +1780,8 @@ "ai-chat": "Czat AI", "task-list": "Lista zadań", "new-feature": "Nowość", - "collections": "Kolekcje" + "collections": "Kolekcje", + "spreadsheet": "Arkusz" }, "protect_note": { "toggle-on": "Chroń notatkę", diff --git a/apps/client/src/translations/ru/translation.json b/apps/client/src/translations/ru/translation.json index c3e1344e2d..36bc11cd9f 100644 --- a/apps/client/src/translations/ru/translation.json +++ b/apps/client/src/translations/ru/translation.json @@ -257,7 +257,7 @@ "collapseExpand": "свернуть/развернуть узел", "notSet": "не установлено", "goBackForwards": "назад / вперед в истории", - "showJumpToNoteDialog": "показать окно \"Перейти к\"", + "showJumpToNoteDialog": "Перейти к \"Перейти к\" окно", "scrollToActiveNote": "прокрутка к активной заметке", "jumpToParentNote": "переход к родительской заметке", "collapseWholeTree": "свернуть все дерево заметок", @@ -471,7 +471,7 @@ "calendar_root": "отмечает заметку, которая должна использоваться в качестве корневой для заметок дня. Только одна должна быть отмечена как таковая.", "archived": "заметки с этой меткой не будут отображаться в результатах поиска по умолчанию (а также в диалоговых окнах «Перейти к», «Добавить ссылку» и т. д.).", "exclude_from_export": "заметки (с их поддеревьями) не будут включены ни в один экспорт заметок", - "run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:\n
    \n
  • frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.
  • \n
  • mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.
  • \n
  • backendStartup — при запуске бэкенда Trilium.
  • \n
  • hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку runAtHour.
  • \n
  • daily — запускать раз в день.
  • \n
", + "run": "определяет, при каких событиях должен запускаться скрипт. Возможные значения:
    \n
  • frontendStartup — при запуске (или обновлении) фронтенда Trilium, но не на мобильном устройстве.
  • \n
  • mobileStartup — при запуске (или обновлении) фронтенда Trilium на мобильном устройстве.
  • \n
  • backendStartup — при запуске бэкенда Trilium.
  • \n
  • hourly — запускать каждый час. Для указания времени можно использовать дополнительную метку runAtHour.
  • \n
  • daily — запускать раз в день.
", "run_on_instance": "Определить, на каком экземпляре Trilium это должно выполняться. По умолчанию — для всех экземпляров.", "run_at_hour": "В какой час это должно выполняться? Следует использовать вместе с #run=hourly. Можно задать несколько раз для большего количества запусков в течение дня.", "disable_inclusion": "скрипты с этой меткой не будут включены в выполнение родительского скрипта.", @@ -594,7 +594,8 @@ "display-week-numbers": "Отображать номера недель", "hide-weekends": "Скрыть выходные", "raster": "Растр", - "show-scale": "Показать масштаб" + "show-scale": "Показать масштаб", + "show-labels": "Показать названия маркеров" }, "editorfeatures": { "note_completion_enabled": "Включить автодополнение", @@ -782,7 +783,13 @@ "shared-indicator-tooltip": "Эта заметка опубликована", "shared-indicator-tooltip-with-url": "Эта заметка доступно публично по адресу: {{- url}}", "subtree-hidden-moved-description-other": "В дереве, к которому относится эта заметка, скрыты дочерние заметки.", - "subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве." + "subtree-hidden-moved-description-collection": "Эта коллекция скрывает свои дочерние заметки в дереве.", + "clone-indicator-tooltip": "У этой заметки {{- count}} родителей: {{- parents}}", + "clone-indicator-tooltip-single": "Эта заметка клонирована (1 дополнительный родитель: {{- parent}})", + "subtree-hidden-moved-title": "Добавлено в {{title}}", + "subtree-hidden-tooltip_one": "{{count}} дочерняя заметка скрыта", + "subtree-hidden-tooltip_few": "Скрыто {{count}} дочерних заметок", + "subtree-hidden-tooltip_many": "Скрыто {{count}} дочерних заметок" }, "quick-search": { "no-results": "Результаты не найдены", @@ -826,7 +833,9 @@ "mind-map": "Mind Map", "geo-map": "Географическая карта", "task-list": "Список задач", - "confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?" + "confirm-change": "Не рекомендуется менять тип заметки, если её содержимое не пустое. Вы всё равно хотите продолжить?", + "ai-chat": "Чат с ИИ", + "spreadsheet": "Электронная таблица" }, "tree-context-menu": { "open-in-popup": "Быстрое редактирование", @@ -1153,7 +1162,8 @@ "search_note_saved": "Заметка с настройкой поиска сохранена в {{- notePathTitle}}", "unknown_search_option": "Неизвестный параметр поиска {{searchOptionName}}", "actions_executed": "Действия выполнены.", - "view_options": "Просмотреть опции:" + "view_options": "Просмотреть опции:", + "option": "опция" }, "ancestor": { "depth_label": "глубина", @@ -1403,7 +1413,8 @@ "type_text_to_filter": "Введите текст для фильтрации сочетаний клавиш...", "reload_app": "Перезагрузить приложение, чтобы применить изменения", "confirm_reset": "Вы действительно хотите сбросить все сочетания клавиш до значений по умолчанию?", - "set_all_to_default": "Установить все сочетания клавиш по умолчанию" + "set_all_to_default": "Установить все сочетания клавиш по умолчанию", + "no_results": "Не найдено ярлыков, соответствующих '{{filter}}'" }, "sync_2": { "timeout_unit": "миллисекунд", @@ -1713,7 +1724,8 @@ "delete_this_note": "Удалить эту заметку", "insert_child_note": "Вставить дочернюю заметку", "note_revisions": "История изменений", - "content_language_switcher": "Язык содержимого: {{language}}" + "content_language_switcher": "Язык содержимого: {{language}}", + "backlinks": "Ссылки" }, "svg_export_button": { "button_title": "Экспортировать диаграмму как SVG" @@ -1790,7 +1802,8 @@ }, "search_result": { "no_notes_found": "По заданным параметрам поиска заметки не найдены.", - "search_not_executed": "Поиск ещё не выполнен. Нажмите кнопку «Поиск» выше, чтобы увидеть результаты." + "search_not_executed": "Поиск ещё не выполнен.", + "search_now": "Искать сейчас" }, "empty": { "search_placeholder": "поиск заметки по ее названию", @@ -1988,10 +2001,12 @@ "print_report_collection_content_few": "{{count}} заметки в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.", "print_report_collection_content_many": "{{count}} заметок в коллекции не удалось распечатать, поскольку они не поддерживаются или защищены.", "print_report_collection_details_button": "Подробнее", - "print_report_collection_details_ignored_notes": "Пропущенные заметки" + "print_report_collection_details_ignored_notes": "Пропущенные заметки", + "print_report_error_title": "Не удалось напечатать", + "print_report_stack_trace": "Трассировка стека" }, "book": { - "no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего. Подробности см. в wiki.", + "no_children_help": "В этой коллекции нет дочерних заметок, поэтому отображать нечего.", "drag_locked_title": "Защищено от изменения", "drag_locked_message": "Перетаскивание не допускается, так как коллекция защищена от редактирования." }, @@ -2007,7 +2022,9 @@ "rendering_error": "Невозможно отобразить содержимое из-за ошибки." }, "pagination": { - "total_notes": "{{count}} заметок" + "total_notes": "{{count}} заметок", + "prev_page": "Предыдущая страница", + "next_page": "Следующая страница" }, "status_bar": { "attributes_one": "{{count}} атрибут", @@ -2137,5 +2154,49 @@ }, "platform_indicator": { "available_on": "Доступно для {{platform}}" + }, + "render": { + "setup_title": "Отобразить настраиваемый HTML или Preact JSX в этой заметке", + "setup_create_sample_preact": "Создать образец заметки с помощью Preact", + "setup_create_sample_html": "Создать образец заметки с помощью HTML", + "setup_sample_created": "Образец заметки был создан в качестве дочерней записи.", + "disabled_description": "Эти заметки для рендера поступают из внешнего источника. Чтобы защитить вас от вредоносного содержимого, они не включены по умолчанию. Убедитесь, что вы доверяете источнику до его включения.", + "disabled_button_enable": "Включить заметки для рендера" + }, + "web_view_setup": { + "title": "Создайте живой просмотр веб-страницы прямо в Trilium", + "url_placeholder": "Введите или вставьте адрес сайта, например https://triliumnotes.org", + "create_button": "Создать веб-просмотр", + "invalid_url_title": "Неверный адрес", + "invalid_url_message": "Введите корректный веб-адрес, например https://triliumnotes.org.", + "disabled_description": "Этот веб-просмотр был импортирован из внешнего источника. Чтобы защитить вас от фишинга или вредоносного контента, он не загружается автоматически. Вы можете включить его, если доверяете источнику.", + "disabled_button_enable": "Включить просмотр веб-страниц" + }, + "active_content_badges": { + "type_icon_pack": "Набор иконок", + "type_backend_script": "Бэкенд скрипт", + "type_frontend_script": "Фронтенд скрипт", + "type_widget": "Виджет", + "type_app_css": "Пользовательский CSS", + "type_render_note": "Заметка для рендера", + "type_web_view": "Просмотр веб-страницы", + "type_app_theme": "Пользовательская тема", + "toggle_tooltip_enable_tooltip": "Нажмите, чтобы включить этот {{type}}.", + "toggle_tooltip_disable_tooltip": "Нажмите, чтобы выключить этот {{type}}.", + "menu_docs": "Открытая документация", + "menu_execute_now": "Выполнить скрипт сейчас", + "menu_run": "Выполнять автоматически", + "menu_run_disabled": "Вручную", + "menu_run_backend_startup": "При запуске бэкенда", + "menu_run_hourly": "Ежечасно", + "menu_run_daily": "Ежедневно", + "menu_run_frontend_startup": "Когда запускается интерфейс ПК", + "menu_run_mobile_startup": "При запуске мобильного интерфейса", + "menu_change_to_widget": "Изменить виджет", + "menu_change_to_frontend_script": "Перейти к фронтенд скрипту", + "menu_theme_base": "Базовая тема" + }, + "setup_form": { + "more_info": "Узнать больше" } } diff --git a/apps/client/src/translations/sv/translation.json b/apps/client/src/translations/sv/translation.json index c5ec9a096b..9e4bc56333 100644 --- a/apps/client/src/translations/sv/translation.json +++ b/apps/client/src/translations/sv/translation.json @@ -3,6 +3,38 @@ "title": "Om Trilium Notes", "homepage": "Hemsida:", "app_version": "App version:", - "db_version": "DB version:" + "db_version": "DB version:", + "sync_version": "Sync version:", + "build_date": "Bygg datum:", + "build_revision": "Bygg version:", + "data_directory": "Data sökväg:" + }, + "toast": { + "critical-error": { + "title": "Kritiskt fel", + "message": "Ett kritiskt fel har inträffat som förhindrar klientprogrammet från att starta:\n\n{{message}}\n\nDetta beror troligen på att ett skript har misslyckats på ett oväntat sätt. Försök att starta programmet i felsäkert läge och åtgärda problemet." + }, + "widget-error": { + "title": "Misslyckades att starta widget", + "message-custom": "Anpassad widget från anteckning med ID \"{{id}}\", med rubrik \"{{title}}\" kunde inte startas på grund av:\n\n{{message}}", + "message-unknown": "Okänd widget kunde inte startas på grund av:\n\n{{message}}" + }, + "bundle-error": { + "title": "Misslyckades att starta ett anpassat skript", + "message": "Skript kunde inte startas på grund av:\n\n{{message}}" + }, + "widget-list-error": { + "title": "Misslyckades att hämta widget-listan från servern" + }, + "widget-render-error": { + "title": "Misslyckades att renderera en anpassad React-widget" + }, + "widget-missing-parent": "Anpassad widget saknar '{{property}}', som måste vara definierad.\n\nOm skriptet är avsett att köras utan gränssnitt, använd '#run-frontendStartup' istället.", + "open-script-note": "Öppna skriptanteckning", + "scripting-error": "Fel i anpassat skript: {{title}}" + }, + "add_link": { + "add_link": "Infoga länk", + "help_on_links": "Hjälp om länkar" } } diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index f095101a9d..e3ec0d07cc 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -1496,7 +1496,8 @@ "task-list": "任務列表", "new-feature": "新增", "collections": "集合", - "ai-chat": "AI 聊天" + "ai-chat": "AI 聊天", + "spreadsheet": "試算表" }, "protect_note": { "toggle-on": "保護筆記", diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 206b2a325a..ea43851359 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -40,6 +40,21 @@ export default function NoteDetail() { const widgetRequestId = useRef(0); const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile"); + // Defer loading for tabs that haven't been active yet (e.g. on app refresh). + // Special contexts (ntxId starting with "_", e.g. popup editor) are always considered active. + const isSpecialContext = ntxId?.startsWith("_") ?? false; + const [ hasTabBeenActive, setHasTabBeenActive ] = useState(() => isSpecialContext || (noteContext?.isActive() ?? false)); + useEffect(() => { + if (!hasTabBeenActive && noteContext?.isActive()) { + setHasTabBeenActive(true); + } + }, [ noteContext, hasTabBeenActive ]); + useTriliumEvent("activeNoteChanged", ({ ntxId: eventNtxId }) => { + if (eventNtxId === ntxId && !hasTabBeenActive) { + setHasTabBeenActive(true); + } + }); + const props: TypeWidgetProps = { note: note!, viewScope, @@ -49,7 +64,7 @@ export default function NoteDetail() { }; useEffect(() => { - if (!type) return; + if (!type || !hasTabBeenActive) return; const requestId = ++widgetRequestId.current; if (!noteTypesToRender[type]) { @@ -68,7 +83,7 @@ export default function NoteDetail() { } else { setActiveNoteType(type); } - }, [ note, viewScope, type, noteTypesToRender ]); + }, [ note, viewScope, type, noteTypesToRender, hasTabBeenActive ]); // Detect note type changes. useTriliumEvent("entitiesReloaded", async ({ loadResults }) => { @@ -247,9 +262,8 @@ function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: { useEffect(() => { if (isVisible) { setCachedProps(props); - } else { - // Do nothing, keep the old props. } + // When not visible, keep the old props to avoid re-rendering in the background. }, [ props, isVisible ]); const typeMapping = TYPE_MAPPINGS[type]; @@ -260,7 +274,7 @@ function NoteDetailWrapper({ Element, type, isVisible, isFullHeight, props }: { height: isFullHeight ? "100%" : "" }} > - { } + ); } diff --git a/apps/client/src/widgets/collections/board/api.ts b/apps/client/src/widgets/collections/board/api.ts index af88f935e5..525f74d6f1 100644 --- a/apps/client/src/widgets/collections/board/api.ts +++ b/apps/client/src/widgets/collections/board/api.ts @@ -1,5 +1,5 @@ import { BulkAction } from "@triliumnext/commons"; -import { BoardViewData } from "."; + import appContext from "../../../components/app_context"; import FNote from "../../../entities/fnote"; import attributes from "../../../services/attributes"; @@ -9,6 +9,7 @@ import froca from "../../../services/froca"; import { t } from "../../../services/i18n"; import note_create from "../../../services/note_create"; import server from "../../../services/server"; +import { BoardViewData } from "."; import { ColumnMap } from "./data"; export default class BoardApi { @@ -35,13 +36,11 @@ export default class BoardApi { async createNewItem(column: string, title: string) { try { - // Get the parent note path - const parentNotePath = this.parentNote.noteId; - // Create a new note as a child of the parent note - const { note: newNote, branch: newBranch } = await note_create.createNote(parentNotePath, { + const { note: newNote, branch: newBranch } = await note_create.createNote(this.parentNote.noteId, { activate: false, - title + title, + isProtected: this.parentNote.isProtected }); if (newNote && newBranch) { @@ -87,7 +86,7 @@ export default class BoardApi { const action: BulkAction = this.isRelationMode ? { name: "deleteRelation", relationName: this.statusAttribute } - : { name: "deleteLabel", labelName: this.statusAttribute } + : { name: "deleteLabel", labelName: this.statusAttribute }; await executeBulkActions(noteIds, [ action ]); this.viewConfig.columns = (this.viewConfig.columns ?? []).filter(col => col.value !== column); this.saveConfig(this.viewConfig); @@ -99,7 +98,7 @@ export default class BoardApi { // Change the value in the notes. const action: BulkAction = this.isRelationMode ? { name: "updateRelationTarget", relationName: this.statusAttribute, targetNoteId: newValue } - : { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue } + : { name: "updateLabelValue", labelName: this.statusAttribute, labelValue: newValue }; await executeBulkActions(noteIds, [ action ]); // Rename the column in the persisted data. @@ -137,9 +136,9 @@ export default class BoardApi { } async insertRowAtPosition( - column: string, - relativeToBranchId: string, - direction: "before" | "after") { + column: string, + relativeToBranchId: string, + direction: "before" | "after") { const { note, branch } = await note_create.createNote(this.parentNote.noteId, { activate: false, targetBranchId: relativeToBranchId, @@ -179,9 +178,8 @@ export default class BoardApi { if (!note) return; if (this.isRelationMode) { return attributes.removeOwnedRelationByName(note, this.statusAttribute); - } else { - return attributes.removeOwnedLabelByName(note, this.statusAttribute); } + return attributes.removeOwnedLabelByName(note, this.statusAttribute); } async moveWithinBoard(noteId: string, sourceBranchId: string, sourceIndex: number, targetIndex: number, sourceColumn: string, targetColumn: string) { diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css index 73bd997a6e..28f64c9397 100644 --- a/apps/client/src/widgets/collections/board/index.css +++ b/apps/client/src/widgets/collections/board/index.css @@ -14,8 +14,7 @@ height: 100%; display: flex; gap: 1em; - margin-inline: var(--content-margin-inline); - padding-block: 4px; + padding: 4px var(--content-margin-inline); align-items: flex-start; overflow-x: auto; } @@ -42,7 +41,11 @@ body.mobile .board-view-container { body.mobile .board-view-container .board-column { width: 75vw; max-width: 300px; - scroll-snap-align: center; +} + +body.mobile .board-view-container .board-column, +body.mobile .board-view-container .board-add-column { + scroll-snap-align: center; } .board-view-container .board-column.drag-over { diff --git a/apps/client/src/widgets/collections/calendar/api.ts b/apps/client/src/widgets/collections/calendar/api.ts index 79df65e4c1..b74f5047b6 100644 --- a/apps/client/src/widgets/collections/calendar/api.ts +++ b/apps/client/src/widgets/collections/calendar/api.ts @@ -1,8 +1,8 @@ -import { AttributeRow, CreateChildrenResponse } from "@triliumnext/commons"; +import { AttributeRow } from "@triliumnext/commons"; import FNote from "../../../entities/fnote"; import { setAttribute, setLabel } from "../../../services/attributes"; -import server from "../../../services/server"; +import note_create from "../../../services/note_create"; interface NewEventOpts { title: string; @@ -51,11 +51,13 @@ export async function newEvent(parentNote: FNote, { title, startDate, endDate, s } // Create the note. - await server.post(`notes/${parentNote.noteId}/children?target=into`, { + await note_create.createNote(parentNote.noteId, { title, + isProtected: parentNote.isProtected, content: "", type: "text", - attributes + attributes, + activate: false }, componentId); } diff --git a/apps/client/src/widgets/collections/geomap/api.ts b/apps/client/src/widgets/collections/geomap/api.ts index 5f73415605..76c05e637f 100644 --- a/apps/client/src/widgets/collections/geomap/api.ts +++ b/apps/client/src/widgets/collections/geomap/api.ts @@ -1,10 +1,11 @@ import type { LatLng, LeafletMouseEvent } from "leaflet"; -import { LOCATION_ATTRIBUTE } from "."; + +import FNote from "../../../entities/fnote"; import attributes from "../../../services/attributes"; import { prompt } from "../../../services/dialog"; -import server from "../../../services/server"; import { t } from "../../../services/i18n"; -import { CreateChildrenResponse } from "@triliumnext/commons"; +import note_create from "../../../services/note_create"; +import { LOCATION_ATTRIBUTE } from "."; const CHILD_NOTE_ICON = "bx bx-pin"; @@ -13,16 +14,20 @@ export async function moveMarker(noteId: string, latLng: LatLng | null) { await attributes.setLabel(noteId, LOCATION_ATTRIBUTE, value); } -export async function createNewNote(noteId: string, e: LeafletMouseEvent) { +export async function createNewNote(parentNote: FNote, e: LeafletMouseEvent) { const title = await prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); if (title?.trim()) { - const { note } = await server.post(`notes/${noteId}/children?target=into`, { + await note_create.createNote(parentNote.noteId, { title, content: "", - type: "text" + type: "text", + activate: false, + isProtected: parentNote.isProtected, + attributes: [ + { type: "label", name: LOCATION_ATTRIBUTE, value: [e.latlng.lat, e.latlng.lng].join(",") }, + { type: "label", name: "iconClass", value: CHILD_NOTE_ICON } + ] }); - attributes.setLabel(note.noteId, "iconClass", CHILD_NOTE_ICON); - moveMarker(note.noteId, e.latlng); } } diff --git a/apps/client/src/widgets/collections/geomap/context_menu.ts b/apps/client/src/widgets/collections/geomap/context_menu.ts index 47026566fc..b4ae723877 100644 --- a/apps/client/src/widgets/collections/geomap/context_menu.ts +++ b/apps/client/src/widgets/collections/geomap/context_menu.ts @@ -1,12 +1,14 @@ import type { LatLng, LeafletMouseEvent } from "leaflet"; + import appContext, { type CommandMappings } from "../../../components/app_context.js"; +import FNote from "../../../entities/fnote.js"; import contextMenu, { type MenuItem } from "../../../menus/context_menu.js"; -import linkContextMenu from "../../../menus/link_context_menu.js"; import NoteColorPicker from "../../../menus/custom-items/NoteColorPicker.jsx"; -import { t } from "../../../services/i18n.js"; -import { createNewNote } from "./api.js"; +import linkContextMenu from "../../../menus/link_context_menu.js"; import { copyTextWithToast } from "../../../services/clipboard_ext.js"; +import { t } from "../../../services/i18n.js"; import link from "../../../services/link.js"; +import { createNewNote } from "./api.js"; export default function openContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) { let items: MenuItem[] = [ @@ -44,7 +46,7 @@ export default function openContextMenu(noteId: string, e: LeafletMouseEvent, is }); } -export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEditable: boolean) { +export function openMapContextMenu(note: FNote, e: LeafletMouseEvent, isEditable: boolean) { let items: MenuItem[] = [ ...buildGeoLocationItem(e) ]; @@ -55,10 +57,10 @@ export function openMapContextMenu(noteId: string, e: LeafletMouseEvent, isEdita { kind: "separator" }, { title: t("geo-map-context.add-note"), - handler: () => createNewNote(noteId, e), + handler: () => createNewNote(note, e), uiIcon: "bx bx-plus" } - ] + ]; } contextMenu.show({ diff --git a/apps/client/src/widgets/collections/geomap/index.tsx b/apps/client/src/widgets/collections/geomap/index.tsx index 8be547fa3a..cb4b33b5af 100644 --- a/apps/client/src/widgets/collections/geomap/index.tsx +++ b/apps/client/src/widgets/collections/geomap/index.tsx @@ -93,14 +93,14 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM const onClick = useCallback(async (e: LeafletMouseEvent) => { if (state === State.NewNote) { toast.closePersistent("geo-new-note"); - await createNewNote(note.noteId, e); + await createNewNote(note, e); setState(State.Normal); } - }, [ state ]); + }, [ note, state ]); const onContextMenu = useCallback((e: LeafletMouseEvent) => { - openMapContextMenu(note.noteId, e, !isReadOnly); - }, [ note.noteId, isReadOnly ]); + openMapContextMenu(note, e, !isReadOnly); + }, [ note, isReadOnly ]); // Dragging const containerRef = useRef(null); diff --git a/apps/client/src/widgets/collections/presentation/index.tsx b/apps/client/src/widgets/collections/presentation/index.tsx index 0934108521..3c7b236cab 100644 --- a/apps/client/src/widgets/collections/presentation/index.tsx +++ b/apps/client/src/widgets/collections/presentation/index.tsx @@ -2,8 +2,8 @@ import "./index.css"; import { RefObject } from "preact"; import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; -import Reveal from "reveal.js"; -import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw"; +import Reveal, { RevealApi } from "reveal.js"; +import slideBaseStylesheet from "reveal.js/reveal.css?raw"; import { openInCurrentNoteContext } from "../../../components/note_context"; import FNote from "../../../entities/fnote"; @@ -20,7 +20,7 @@ import { DEFAULT_THEME, loadPresentationTheme } from "./themes"; export default function PresentationView({ note, noteIds, media, onReady, onProgressChanged }: ViewModeProps<{}>) { const [ presentation, setPresentation ] = useState(); const containerRef = useRef(null); - const [ api, setApi ] = useState(); + const [ api, setApi ] = useState(); const stylesheets = usePresentationStylesheets(note, media); function refresh() { @@ -98,7 +98,7 @@ function usePresentationStylesheets(note: FNote, media: ViewModeMedia) { return stylesheets; } -function ButtonOverlay({ containerRef, api }: { containerRef: RefObject, api: Reveal.Api | undefined }) { +function ButtonOverlay({ containerRef, api }: { containerRef: RefObject, api: RevealApi | undefined }) { const [ isOverviewActive, setIsOverviewActive ] = useState(false); useEffect(() => { if (!api) return; @@ -144,9 +144,9 @@ function ButtonOverlay({ containerRef, api }: { containerRef: RefObject void }) { +function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: RevealApi | undefined) => void }) { const containerRef = useRef(null); - const [revealApi, setRevealApi] = useState(); + const [revealApi, setRevealApi] = useState(); useEffect(() => { if (!containerRef.current) return; @@ -222,7 +222,7 @@ function getNoteIdFromSlide(slide: HTMLElement | undefined) { return slide.dataset.noteId; } -function rewireLinks(container: HTMLElement, api: Reveal.Api) { +function rewireLinks(container: HTMLElement, api: RevealApi) { const links = container.querySelectorAll("a.reference-link"); for (const link of links) { link.addEventListener("click", () => { diff --git a/apps/client/src/widgets/collections/presentation/themes.ts b/apps/client/src/widgets/collections/presentation/themes.ts index 414472d562..ebd2a9c341 100644 --- a/apps/client/src/widgets/collections/presentation/themes.ts +++ b/apps/client/src/widgets/collections/presentation/themes.ts @@ -3,49 +3,49 @@ export const DEFAULT_THEME = "white"; const themes = { black: { name: "Black", - loadTheme: () => import("reveal.js/dist/theme/black.css?raw") + loadTheme: () => import("reveal.js/theme/black.css?raw") }, white: { name: "White", - loadTheme: () => import("reveal.js/dist/theme/white.css?raw") + loadTheme: () => import("reveal.js/theme/white.css?raw") }, beige: { name: "Beige", - loadTheme: () => import("reveal.js/dist/theme/beige.css?raw") + loadTheme: () => import("reveal.js/theme/beige.css?raw") }, serif: { name: "Serif", - loadTheme: () => import("reveal.js/dist/theme/serif.css?raw") + loadTheme: () => import("reveal.js/theme/serif.css?raw") }, simple: { name: "Simple", - loadTheme: () => import("reveal.js/dist/theme/simple.css?raw") + loadTheme: () => import("reveal.js/theme/simple.css?raw") }, solarized: { name: "Solarized", - loadTheme: () => import("reveal.js/dist/theme/solarized.css?raw") + loadTheme: () => import("reveal.js/theme/solarized.css?raw") }, moon: { name: "Moon", - loadTheme: () => import("reveal.js/dist/theme/moon.css?raw") + loadTheme: () => import("reveal.js/theme/moon.css?raw") }, dracula: { name: "Dracula", - loadTheme: () => import("reveal.js/dist/theme/dracula.css?raw") + loadTheme: () => import("reveal.js/theme/dracula.css?raw") }, sky: { name: "Sky", - loadTheme: () => import("reveal.js/dist/theme/sky.css?raw") + loadTheme: () => import("reveal.js/theme/sky.css?raw") }, blood: { name: "Blood", - loadTheme: () => import("reveal.js/dist/theme/blood.css?raw") + loadTheme: () => import("reveal.js/theme/blood.css?raw") } } as const; export function getPresentationThemes() { return Object.entries(themes).map(([ id, theme ]) => ({ - id: id, + id, name: theme.name })); } diff --git a/apps/client/src/widgets/collections/table/index.tsx b/apps/client/src/widgets/collections/table/index.tsx index 87745b3e2d..b4c49faf70 100644 --- a/apps/client/src/widgets/collections/table/index.tsx +++ b/apps/client/src/widgets/collections/table/index.tsx @@ -18,14 +18,14 @@ import useRowTableEditing from "./row_editing"; import { TableData } from "./rows"; import Tabulator from "./tabulator"; -export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps) { +export default function TableView({ note, noteIds, viewConfig, saveConfig }: ViewModeProps) { const tabulatorRef = useRef(null); const parentComponent = useContext(ParentComponent); const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget().contentSized()); const contextMenuEvents = useContextMenu(note, parentComponent, tabulatorRef); const persistenceProps = usePersistence(viewConfig, saveConfig); - const rowEditingEvents = useRowTableEditing(tabulatorRef, attributeDetailWidget, notePath); + const rowEditingEvents = useRowTableEditing(tabulatorRef, attributeDetailWidget, note); const { newAttributePosition, resetNewAttributePosition } = useColTableEditing(tabulatorRef, attributeDetailWidget, note); const { columnDefs, rowData, movableRows, hasChildren } = useData(note, noteIds, viewConfig, newAttributePosition, resetNewAttributePosition); const dataTreeProps = useMemo(() => { diff --git a/apps/client/src/widgets/collections/table/row_editing.ts b/apps/client/src/widgets/collections/table/row_editing.ts index 22ef0e7e48..c4df69e7ae 100644 --- a/apps/client/src/widgets/collections/table/row_editing.ts +++ b/apps/client/src/widgets/collections/table/row_editing.ts @@ -1,24 +1,27 @@ -import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; -import { CommandListenerData } from "../../../components/app_context"; -import note_create, { CreateNoteOpts } from "../../../services/note_create"; -import { useLegacyImperativeHandlers } from "../../react/hooks"; import { RefObject } from "preact"; -import { setAttribute, setLabel } from "../../../services/attributes"; -import froca from "../../../services/froca"; -import server from "../../../services/server"; -import branches from "../../../services/branches"; -import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import { EventCallBackMethods, RowComponent, Tabulator } from "tabulator-tables"; -export default function useRowTableEditing(api: RefObject, attributeDetailWidget: AttributeDetailWidget, parentNotePath: string): Partial { +import { CommandListenerData } from "../../../components/app_context"; +import FNote from "../../../entities/fnote"; +import { setAttribute, setLabel } from "../../../services/attributes"; +import branches from "../../../services/branches"; +import froca from "../../../services/froca"; +import note_create, { CreateNoteOpts } from "../../../services/note_create"; +import server from "../../../services/server"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import { useLegacyImperativeHandlers } from "../../react/hooks"; + +export default function useRowTableEditing(api: RefObject, attributeDetailWidget: AttributeDetailWidget, parentNote: FNote): Partial { // Adding new rows useLegacyImperativeHandlers({ addNewRowCommand({ customOpts, parentNotePath: customNotePath }: CommandListenerData<"addNewRow">) { - const notePath = customNotePath ?? parentNotePath; + const notePath = customNotePath ?? parentNote.noteId; if (notePath) { const opts: CreateNoteOpts = { activate: false, + isProtected: parentNote.isProtected, ...customOpts - } + }; note_create.createNote(notePath, opts).then(({ branch }) => { if (branch) { setTimeout(() => { @@ -26,7 +29,7 @@ export default function useRowTableEditing(api: RefObject, attributeD focusOnBranch(api.current, branch?.branchId); }, 100); } - }) + }); } } }); @@ -91,14 +94,14 @@ function focusOnBranch(api: Tabulator, branchId: string) { } function findRowDataById(rows: RowComponent[], branchId: string): RowComponent | null { - for (let row of rows) { + for (const row of rows) { const item = row.getIndex() as string; if (item === branchId) { return row; } - let found = findRowDataById(row.getTreeChildren(), branchId); + const found = findRowDataById(row.getTreeChildren(), branchId); if (found) return found; } return null; diff --git a/apps/client/src/widgets/dialogs/revisions.tsx b/apps/client/src/widgets/dialogs/revisions.tsx index 41d22864f3..66ce763a3d 100644 --- a/apps/client/src/widgets/dialogs/revisions.tsx +++ b/apps/client/src/widgets/dialogs/revisions.tsx @@ -272,7 +272,8 @@ function RevisionContent({ noteContent, revisionItem, fullRevision, showDiff }: return ; case "canvas": case "mindMap": - case "mermaid": { + case "mermaid": + case "spreadsheet": { const encodedTitle = encodeURIComponent(revisionItem.title); return + {text}{" "} + + {templates.map(sample => ( + { + await server.put(`notes/${note.noteId}/data`, { + content: sample.content + }); + }} + /> + ))} + + ); +} diff --git a/apps/client/src/widgets/note_types.tsx b/apps/client/src/widgets/note_types.tsx index 687bfdfe9c..b80d4d545e 100644 --- a/apps/client/src/widgets/note_types.tsx +++ b/apps/client/src/widgets/note_types.tsx @@ -84,7 +84,7 @@ export const TYPE_MAPPINGS: Record = { printable: true }, mermaid: { - view: () => import("./type_widgets/Mermaid"), + view: () => import("./type_widgets/mermaid/Mermaid"), className: "note-detail-mermaid", printable: true, isFullHeight: true @@ -143,7 +143,7 @@ export const TYPE_MAPPINGS: Record = { isFullHeight: true }, spreadsheet: { - view: () => import("./type_widgets/Spreadsheet"), + view: () => import("./type_widgets/spreadsheet/Spreadsheet"), className: "note-detail-spreadsheet", printable: true, isFullHeight: true diff --git a/apps/client/src/widgets/note_wrapper.ts b/apps/client/src/widgets/note_wrapper.ts index 80fc42c6d2..ccaa5614d9 100644 --- a/apps/client/src/widgets/note_wrapper.ts +++ b/apps/client/src/widgets/note_wrapper.ts @@ -83,7 +83,7 @@ export default class NoteWrapperWidget extends FlexContainer { return true; } - if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/"))) { + if (note.type === "file" && (note.mime === "application/pdf" || note.mime.startsWith("video/") || note.mime.startsWith("audio/"))) { return true; } @@ -108,7 +108,7 @@ export default class NoteWrapperWidget extends FlexContainer { return true; } - if (note.type === "file" && MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime)) { + if (note.type === "file" && (MIME_TYPES_WITH_BACKGROUND_EFFECTS.includes(note.mime) || note.mime.startsWith("audio/"))) { return true; } diff --git a/apps/client/src/widgets/react/NoItems.css b/apps/client/src/widgets/react/NoItems.css index f3cc6c6553..2bf1d4e067 100644 --- a/apps/client/src/widgets/react/NoItems.css +++ b/apps/client/src/widgets/react/NoItems.css @@ -8,6 +8,7 @@ color: var(--muted-text-color); height: 100%; text-align: center; + white-space: pre-line; .tn-icon { font-size: 4em; diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 7616b9d9b1..46d83a5614 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -98,6 +98,7 @@ export interface SavedData { mime: string; content: string; position: number; + encoding?: "base64"; }[]; } diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 7810cfe1ae..46a0d9d6b7 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -75,7 +75,7 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote const noteType = useNoteProperty(note, "type") ?? ""; const [viewType] = useNoteLabel(note, "viewType"); const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); - const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType); + const isSearchable = ["text", "code", "book", "mindMap", "doc", "spreadsheet"].includes(noteType); const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); const isExportableToImage = ["mermaid", "mindMap"].includes(noteType); const isContentAvailable = note.isContentAvailable(); diff --git a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx index 482cd1a693..4c91c1323b 100644 --- a/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx +++ b/apps/client/src/widgets/ribbon/NoteActionsCustom.tsx @@ -189,7 +189,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); const isSavedSqlite = note.isTriliumSqlite() && !note.isHiddenCompletely(); - const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite) + const isEnabled = ([ "mermaid", "mindMap", "canvas", "spreadsheet" ].includes(note.type) || isSavedSqlite) && note.isContentAvailable() && isDefaultViewMode; return isEnabled && { return ( - change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel') + change.type === 'insert' || change.type === 'remove' || + (change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor)) ); }); if (affectsHeadings) { - setHeadings(extractTocFromTextEditor(textEditor)); + requestAnimationFrame(() => { + setHeadings(extractTocFromTextEditor(textEditor)); + }); } }; diff --git a/apps/client/src/widgets/type_widgets/File.css b/apps/client/src/widgets/type_widgets/File.css index d7f7035c04..54114fe231 100644 --- a/apps/client/src/widgets/type_widgets/File.css +++ b/apps/client/src/widgets/type_widgets/File.css @@ -24,8 +24,7 @@ margin: 10px; } -.note-detail-file > .pdf-preview, -.note-detail-file > .video-preview { +.note-detail-file > .pdf-preview { width: 100%; height: 100%; flex-grow: 100; @@ -38,4 +37,4 @@ right: 15px; width: calc(100% - 30px); transform: translateY(-50%); -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/type_widgets/File.tsx b/apps/client/src/widgets/type_widgets/File.tsx index 0d5b038608..f36ecce854 100644 --- a/apps/client/src/widgets/type_widgets/File.tsx +++ b/apps/client/src/widgets/type_widgets/File.tsx @@ -1,11 +1,11 @@ import "./File.css"; -import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; -import { getUrlForDownload } from "../../services/open"; import Alert from "../react/Alert"; import { useNoteBlob } from "../react/hooks"; +import AudioPreview from "./file/Audio"; import PdfPreview from "./file/Pdf"; +import VideoPreview from "./file/Video"; import { TypeWidgetProps } from "./type_widget"; const TEXT_MAX_NUM_CHARS = 5000; @@ -42,27 +42,6 @@ function TextPreview({ content }: { content: string }) { ); } -function VideoPreview({ note }: { note: FNote }) { - return ( -