Compare commits

..

1 Commits

Author SHA1 Message Date
perfectra1n
51325e2f4a fix(editor): resolve strange stuck indent when a Note has images 2026-01-26 17:03:42 -08:00
115 changed files with 1741 additions and 3799 deletions

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- name: Check if PRs have conflicts
uses: eps1lon/actions-label-merge-conflict@v3
if: ${{ github.repository == vars.REPO_MAIN }}
if: github.repository == ${{ vars.REPO_MAIN }}
with:
dirtyLabel: "merge-conflicts"
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"

View File

@@ -67,7 +67,7 @@ jobs:
- name: Deploy
uses: ./.github/actions/deploy-to-cloudflare-pages
if: ${{ github.repository == vars.REPO_MAIN }}
if: github.repository == ${{ vars.REPO_MAIN }}
with:
project_name: "trilium-docs"
comment_body: "📚 Documentation preview is ready"

View File

@@ -26,7 +26,7 @@ permissions:
jobs:
nightly-electron:
if: ${{ github.repository == vars.REPO_MAIN }}
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy nightly
strategy:
fail-fast: false
@@ -109,7 +109,7 @@ jobs:
path: apps/desktop/upload
nightly-server:
if: ${{ github.repository == vars.REPO_MAIN }}
if: github.repository == ${{ vars.REPO_MAIN }}
name: Deploy server nightly
strategy:
fail-fast: false

View File

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

View File

@@ -27,7 +27,7 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.6.2",
"@preact/signals": "2.6.1",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*",
@@ -43,7 +43,7 @@
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.0",
"globals": "17.2.0",
"globals": "17.0.0",
"i18next": "25.8.0",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
@@ -59,9 +59,9 @@
"mind-elixir": "5.6.1",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.3",
"react-i18next": "16.5.4",
"react-window": "2.2.6",
"preact": "10.28.2",
"react-i18next": "16.5.3",
"react-window": "2.2.5",
"reveal.js": "5.2.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
@@ -78,9 +78,9 @@
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.4.0",
"happy-dom": "20.3.9",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"
"vite-plugin-static-copy": "3.1.5"
}
}

View File

@@ -99,22 +99,15 @@ function initFullScreenDetection(currentWindow: Electron.BrowserWindow) {
}
function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
const material = style.getPropertyValue("--background-material").trim();
if (window.glob.platform === "win32") {
const material = style.getPropertyValue("--background-material");
// TriliumNextTODO: find a nicer way to make TypeScript happy unfortunately TS did not like Array.includes here
const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
if (foundBgMaterialOption) {
currentWindow.setBackgroundMaterial(foundBgMaterialOption);
}
}
if (window.glob.platform === "darwin") {
const bgMaterialOptions = [ "popover", "tooltip", "titlebar", "selection", "menu", "sidebar", "header", "sheet", "window", "hud", "fullscreen-ui", "content", "under-window", "under-page" ] as const;
const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
if (foundBgMaterialOption) {
currentWindow.setVibrancy(foundBgMaterialOption);
}
}
}
/**

View File

@@ -13,14 +13,13 @@
<body id="trilium-app">
<noscript>Trilium requires JavaScript to be enabled.</noscript>
<div id="context-menu-cover"></div>
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
<!-- Required to match the PWA's top bar color with the theme -->
<!-- This works even when the user directly changes --root-background in CSS -->
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
<script src="./src/index.ts" type="module"></script>
<script src="./index.ts" type="module"></script>
<!-- Required for correct loading of scripts in Electron -->
<script>

View File

@@ -30,7 +30,7 @@ async function initJQuery() {
}
async function setupGlob() {
const response = await fetch(`./bootstrap${window.location.search}`);
const response = await fetch(`/bootstrap${window.location.search}`);
const json = await response.json();
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */

View File

@@ -179,6 +179,7 @@ export default class MobileLayout {
new FlexContainer("column")
.contentSized()
.id("mobile-bottom-bar")
.child(new TabRowWidget().css("height", "40px"))
.child(new FlexContainer("row")
.class("horizontal")
.css("height", "53px")

View File

@@ -1,9 +1,8 @@
import { KeyboardActionNames } from "@triliumnext/commons";
import { h, JSX, render } from "preact";
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
import note_tooltip from "../services/note_tooltip.js";
import utils from "../services/utils.js";
import { h, JSX, render } from "preact";
export interface ContextMenuOptions<T> {
x: number;
@@ -63,17 +62,17 @@ export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEve
class ContextMenu {
private $widget: JQuery<HTMLElement>;
private $cover?: JQuery<HTMLElement>;
private $cover: JQuery<HTMLElement>;
private options?: ContextMenuOptions<any>;
private isMobile: boolean;
constructor() {
this.$widget = $("#context-menu-container");
this.$cover = $("#context-menu-cover");
this.$widget.addClass("dropend");
this.isMobile = utils.isMobile();
if (this.isMobile) {
this.$cover = $("#context-menu-cover");
this.$cover.on("click", () => this.hide());
} else {
$(document).on("click", (e) => this.hide());
@@ -92,7 +91,7 @@ class ContextMenu {
}
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
this.$cover?.addClass("show");
this.$cover.addClass("show");
$("body").addClass("context-menu-shown");
this.$widget.empty();
@@ -141,14 +140,16 @@ class ContextMenu {
} else {
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
}
} else if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
// Overflow: right
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
// Overflow: left
left = CONTEXT_MENU_PADDING;
} else {
left = this.options.x - CONTEXT_MENU_OFFSET;
if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
// Overflow: right
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
// Overflow: left
left = CONTEXT_MENU_PADDING;
} else {
left = this.options.x - CONTEXT_MENU_OFFSET;
}
}
this.$widget
@@ -260,7 +261,7 @@ class ContextMenu {
.append(item.title);
if ("badges" in item && item.badges) {
for (const badge of item.badges) {
for (let badge of item.badges) {
const badgeElement = $(`<span class="badge">`).text(badge.title);
if (badge.className) {
@@ -351,7 +352,7 @@ class ContextMenu {
async hide() {
this.options?.onHide?.();
this.$widget.removeClass("show");
this.$cover?.removeClass("show");
this.$cover.removeClass("show");
$("body").removeClass("context-menu-shown");
this.$widget.hide();
}

View File

@@ -49,7 +49,7 @@ function createClassForColor(colorString: string | null) {
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
}
export function parseColor(color: string) {
function parseColor(color: string) {
try {
return Color(color.toLowerCase());
} catch (ex) {
@@ -77,7 +77,7 @@ function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: numb
}
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
export function getHue(color: ColorInstance) {
function getHue(color: ColorInstance) {
const hslColor = color.hsl();
if (hslColor.saturationl() > 0) {
return hslColor.hue();

View File

@@ -224,6 +224,10 @@ body.mobile .modal .modal-dialog {
width: 100%;
}
body.mobile .modal .modal-content {
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
}
.component {
contain: size;
}
@@ -1251,7 +1255,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
z-index: 2500;
z-index: 1000;
background: rgba(0, 0, 0, 0.1);
}
@@ -1610,7 +1614,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
body.mobile .modal-content {
overflow-y: auto;
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
}
body.mobile .modal-footer {
@@ -1666,15 +1669,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
#detail-container {
background: var(--main-background-color);
}
.modal-dialog {
margin: var(--bs-modal-margin);
max-width: 80%;
}
.modal-content {
height: 100%;
}
}
@media (max-width: 991px) {

View File

@@ -40,30 +40,13 @@ body.mobile {
/* #region Mica */
/* Quirk: --background-material is read before "theme-supports-background-effects" class
* is applied. Apply the matterial even if the theme doesn't support it. */
body.background-effects.platform-win32 {
&.layout-vertical {
--background-material: mica;
}
&.layout-horizontal {
--background-material: tabbed;
}
/* Quirk: --background-material is read before "theme-supports-background-effects" class
* is applied. Apply the matterial even if the theme doesn't support it. */
--background-material: tabbed;
}
body.background-effects.platform-darwin {
/** Reference: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc **/
&.layout-vertical {
--background-material: under-window;
}
&.layout-horizontal {
--background-material: hud;
}
}
body.background-effects.theme-supports-background-effects {
body.background-effects.theme-supports-background-effects.platform-win32 {
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
@@ -73,29 +56,33 @@ body.background-effects.theme-supports-background-effects {
--root-background: transparent;
}
body.background-effects.theme-supports-background-effects.layout-vertical {
body.background-effects.platform-win32.layout-vertical {
--background-material: mica;
}
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
--left-pane-background-color: var(--window-background-color-bgfx);
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
--right-pane-background-color: var(--right-pane-background-color-bgfx);
}
body.background-effects.theme-supports-background-effects.layout-horizontal {
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
--gutter-color: var(--left-pane-background-color);
}
body.background-effects.theme-supports-background-effects,
body.background-effects.theme-supports-background-effects #root-widget {
body.background-effects.theme-supports-background-effects.platform-win32,
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
background: var(--window-background-color-bgfx) !important;
}
body.background-effects.theme-supports-background-effects.layout-horizontal #horizontal-main-container,
body.background-effects.theme-supports-background-effects.layout-vertical #vertical-main-container {
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
background-color: var(--root-background);
}
/* Note split with background effects */
body.background-effects.theme-supports-background-effects #center-pane .note-split.bgfx {
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
--note-split-background-color: var(--center-pane-background-color-bgfx);
}
@@ -1067,7 +1054,7 @@ body.layout-horizontal .tab-row-widget-container {
overflow: hidden;
}
body.desktop:not(.background-effects) #root-widget.horizontal-layout {
body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-layout {
background-color: var(--root-background) !important;
}

View File

@@ -28,9 +28,9 @@
},
"open-script-note": "Script-Notiz öffnen",
"widget-render-error": {
"title": "Benutzerdefiniertes React-Widget konnte nicht dargestellt werden"
"title": "Eine externe React Integration konnte nicht dargestellt werden"
},
"widget-missing-parent": "Benutzerdefiniertes Widget hat die erforderliche '{{property}}'-Eigenschaft nicht korrekt definiert.\n\nFalls dieses Skript ohne UI-Element ausgeführt werden soll, benutze stattdessen '#run=frontendStartup'.",
"widget-missing-parent": "Der externen Integration fehlt die erforderliche Eigenschaft '{{property}}'\n\nFalls dieses Skript ohne UI-Element ausgeführt werden soll, benutze stattdessen '#run=frontendStartup'.",
"scripting-error": "Benutzerdefinierter Skriptfehler: {{title}}"
},
"add_link": {
@@ -129,7 +129,7 @@
"scrollToActiveNote": "Scrolle zur aktiven Notiz",
"jumpToParentNote": "Zur übergeordneten Notiz springen",
"collapseWholeTree": "Reduziere den gesamten Notizbaum",
"collapseSubTree": "Zweig einklappen",
"collapseSubTree": "Teilbaum einklappen",
"tabShortcuts": "Tab-Tastenkürzel",
"newTabNoteLink": "auf den Notizlink öffnet die Notiz in einem neuen Tab",
"onlyInDesktop": "Nur im Desktop (Electron Build)",
@@ -230,7 +230,7 @@
"move_to": {
"dialog_title": "Notizen verschieben nach ...",
"notes_to_move": "Notizen zum Verschieben",
"target_parent_note": "Übergeordnete Notiz bestimmen",
"target_parent_note": "Ziel-Elternnotiz",
"search_placeholder": "Suche nach einer Notiz anhand ihres Namens",
"move_button": "Zur ausgewählten Notiz wechseln",
"error_no_path": "Kein Weg, auf den man sich bewegen kann.",
@@ -333,8 +333,8 @@
"target_note_title": "Eine Beziehung ist eine benannte Verbindung zwischen Quellnotiz und Zielnotiz.",
"target_note": "Zielnotiz",
"promoted_title": "Das heraufgestufte Attribut wird deutlich in der Notiz angezeigt.",
"promoted": "Hervorgehoben",
"promoted_alias_title": "Der Name, der in der Benutzeroberfläche für hervorgehobene Attribute angezeigt werden soll.",
"promoted": "Gefördert",
"promoted_alias_title": "Der Name, der in der Benutzeroberfläche für heraufgestufte Attribute angezeigt werden soll.",
"promoted_alias": "Alias",
"multiplicity_title": "Multiplizität definiert, wie viele Attribute mit demselben Namen erstellt werden können maximal 1 oder mehr als 1.",
"multiplicity": "Vielzahl",
@@ -367,7 +367,7 @@
"disable_versioning": "deaktiviert die automatische Versionierung. Nützlich z.B. große, aber unwichtige Notizen z.B. große JS-Bibliotheken, die für die Skripterstellung verwendet werden",
"calendar_root": "Markiert eine Notiz, die als Basis für Tagesnotizen verwendet werden soll. Nur einer sollte als solcher gekennzeichnet sein.",
"archived": "Notizen mit dieser Bezeichnung werden standardmäßig nicht in den Suchergebnissen angezeigt (auch nicht in den Dialogen „Springen zu“, „Link hinzufügen“ usw.).",
"exclude_from_export": "Notizen (mit ihrem Unterbaum) werden nicht im Notizexport inkludiert",
"exclude_from_export": "Notizen (mit ihrem Unterbaum) werden nicht in den Notizexport einbezogen",
"run": "Definiert, bei welchen Ereignissen das Skript ausgeführt werden soll. Mögliche Werte sind:\n<ul>\n<li>frontendStartup - wenn das Trilium-Frontend startet (oder aktualisiert wird), außer auf mobilen Geräten.</li>\n<li>mobileStartup - wenn das Trilium-Frontend auf einem mobilen Gerät startet (oder aktualisiert wird).</li>\n<li>backendStartup - wenn das Trilium-Backend startet</li>\n<li>hourly - einmal pro Stunde ausführen. Du kannst das zusätzliche Label <code>runAtHour</code> verwenden, um die genaue Stunde festzulegen.</li>\n<li>daily - einmal pro Tag ausführen</li>\n</ul>",
"run_on_instance": "Definiere, auf welcher Trilium-Instanz dies ausgeführt werden soll. Standardmäßig alle Instanzen.",
"run_at_hour": "Zu welcher Stunde soll das laufen? Sollte zusammen mit <code>#runu003dhourly</code> verwendet werden. Kann für mehr Läufe im Laufe des Tages mehrfach definiert werden.",
@@ -376,7 +376,7 @@
"sort_direction": "ASC (Standard) oder DESC",
"sort_folders_first": "Ordner (Notizen mit Unternotizen) sollten oben sortiert werden",
"top": "Behalte die angegebene Notiz oben in der übergeordneten Notiz (gilt nur für sortierte übergeordnete Notizen)",
"hide_promoted_attributes": "Hervorgehobene Attribute für diese Notiz ausblenden",
"hide_promoted_attributes": "Heraufgestufte Attribute für diese Notiz ausblenden",
"read_only": "Der Editor befindet sich im schreibgeschützten Modus. Funktioniert nur für Text- und Codenotizen.",
"auto_read_only_disabled": "Text-/Codenotizen können automatisch in den Lesemodus versetzt werden, wenn sie zu groß sind. Du kannst dieses Verhalten für jede einzelne Notiz deaktivieren, indem du diese Beschriftung zur Notiz hinzufügst",
"app_css": "markiert CSS-Notizen, die in die Trilium-Anwendung geladen werden und somit zur Änderung des Aussehens von Trilium verwendet werden können.",
@@ -416,13 +416,13 @@
"toc": "<code>#toc</code> oder <code>#tocu003dshow</code> erzwingen die Anzeige des Inhaltsverzeichnisses, <code>#tocu003dhide</code> erzwingt das Ausblenden. Wenn die Bezeichnung nicht vorhanden ist, wird die globale Einstellung beachtet",
"color": "Definiert die Farbe der Notiz im Notizbaum, in Links usw. Verwende einen beliebigen gültigen CSS-Farbwert wie „rot“ oder #a13d5f",
"keyboard_shortcut": "Definiert eine Tastenkombination, die sofort zu dieser Notiz springt. Beispiel: „Strg+Alt+E“. Erfordert ein Neuladen des Frontends, damit die Änderung wirksam wird.",
"keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Zweig nicht angezeigt werden kann.",
"keep_current_hoisting": "Das Öffnen dieses Links ändert das Hochziehen nicht, selbst wenn die Notiz im aktuell hochgezogenen Unterbaum nicht angezeigt werden kann.",
"execute_button": "Titel der Schaltfläche, welche die aktuelle Codenotiz ausführt",
"execute_description": "Längere Beschreibung der aktuellen Codenotiz, die zusammen mit der Schaltfläche „Ausführen“ angezeigt wird",
"exclude_from_note_map": "Notizen mit dieser Bezeichnung werden in der Notizenkarte ausgeblendet",
"new_notes_on_top": "Neue Notizen werden oben in der übergeordneten Notiz erstellt, nicht unten.",
"hide_highlight_widget": "Widget „Markierungsliste“ ausblenden",
"run_on_note_creation": "Wird ausgeführt, wenn eine Notiz im Backend erstellt wird. Verwende diese Beziehung, wenn du das Skript für alle Notizen ausführen möchtest, die unter einem bestimmten Zweig erstellt wurden. Erstelle es in diesem Fall auf der Stammnotiz und mache es vererbbar. Eine neue Notiz, die innerhalb des Zweigs (beliebige Tiefe) erstellt wird, löst das Skript aus.",
"run_on_note_creation": "Wird ausgeführt, wenn eine Notiz im Backend erstellt wird. Verwende diese Beziehung, wenn du das Skript für alle Notizen ausführen möchtest, die unter einer bestimmten Unternotiz erstellt wurden. Erstelle es in diesem Fall auf der Unternotiz-Stammnotiz und mache es vererbbar. Eine neue Notiz, die innerhalb der Unternotiz (beliebige Tiefe) erstellt wird, löst das Skript aus.",
"run_on_child_note_creation": "Wird ausgeführt, wenn eine neue Notiz unter der Notiz erstellt wird, in der diese Beziehung definiert ist",
"run_on_note_title_change": "Wird ausgeführt, wenn der Notiztitel geändert wird (einschließlich der Notizerstellung)",
"run_on_note_content_change": "Wird ausgeführt, wenn der Inhalt einer Notiz geändert wird (einschließlich der Erstellung von Notizen).",
@@ -433,8 +433,8 @@
"run_on_branch_deletion": "wird ausgeführt, wenn ein Zweig gelöscht wird. Der Zweig ist eine Verknüpfung zwischen der übergeordneten Notiz und der untergeordneten Notiz und wird z. B. gelöscht. beim Verschieben der Notiz (alter Zweig/Link wird gelöscht).",
"run_on_attribute_creation": "wird ausgeführt, wenn für die Notiz ein neues Attribut erstellt wird, das diese Beziehung definiert",
"run_on_attribute_change": " wird ausgeführt, wenn das Attribut einer Notiz geändert wird, die diese Beziehung definiert. Dies wird auch ausgelöst, wenn das Attribut gelöscht wird",
"relation_template": "Die Attribute der Notiz werden auch ohne eine Hierarchische-Beziehung vererbt. Der Inhalt und der Zweig werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
"inherit": "Die Attribute einer Notiz werden auch ohne eine Hierarchische-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributsvererbung in der Dokumentation.",
"relation_template": "Die Attribute der Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Der Inhalt und der Unterbaum der Notiz werden den Instanznotizen hinzugefügt, wenn sie leer sind. Einzelheiten findest du in der Dokumentation.",
"inherit": "Die Attribute einer Notiz werden auch ohne eine Eltern-Kind-Beziehung vererbt. Ein ähnliches Konzept findest du unter Vorlagenbeziehung. Siehe Attributvererbung in der Dokumentation.",
"render_note": "Notizen vom Typ \"HTML-Notiz rendern\" werden mit einer Code-Notiz (HTML oder Skript) gerendert, und es ist notwendig, über diese Beziehung anzugeben, welche Notiz gerendert werden soll",
"widget_relation": "Das Ziel dieser Beziehung wird ausgeführt und als Widget in der Seitenleiste gerendert",
"share_css": "CSS-Hinweis, der in die Freigabeseite eingefügt wird. Die CSS-Notiz muss sich ebenfalls im gemeinsamen Unterbaum befinden. Erwäge auch die Verwendung von „share_hidden_from_tree“ und „share_omit_default_css“.",
@@ -646,7 +646,7 @@
"reset_zoom_level": "Zoomstufe zurücksetzen",
"zoom_in": "Hineinzoomen",
"configure_launchbar": "Konfiguriere die Starterleiste",
"show_shared_notes_subtree": "Zweig „Freigegebene Notizen“ anzeigen",
"show_shared_notes_subtree": "Unterbaum „Freigegebene Notizen“ anzeigen",
"advanced": "Erweitert",
"open_dev_tools": "Öffne die Entwicklungstools",
"open_sql_console": "Öffne die SQL-Konsole",
@@ -655,7 +655,7 @@
"show_backend_log": "Backend-Protokoll anzeigen",
"reload_hint": "Ein Neuladen kann bei einigen visuellen Störungen Abhilfe schaffen, ohne die gesamte App neu starten zu müssen.",
"reload_frontend": "Frontend neu laden",
"show_hidden_subtree": "Versteckten Zweig anzeigen",
"show_hidden_subtree": "Versteckten Teilbaum anzeigen",
"show_help": "Hilfe anzeigen",
"about": "Über Trilium Notes",
"logout": "Abmelden",
@@ -703,8 +703,8 @@
"export_as_image_png": "PNG (Raster)",
"export_as_image_svg": "SVG (Vektor)",
"note_map": "Notizen Karte",
"view_revisions": "Notizrevisionen...",
"advanced": "Erweitert"
"view_revisions": "Änderungshistorie...",
"advanced": "Fortgeschritten"
},
"onclick_button": {
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
@@ -798,7 +798,7 @@
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
"expand_first_level": "Direkte Unterelemente erweitern",
"expand_nth_level": "{{depth}} Ebenen erweitern",
"hide_child_notes": "Unternotizen im Baum ausblenden"
"hide_child_notes": "Unterknoten im Baum ausblenden"
},
"edited_notes": {
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
@@ -842,7 +842,7 @@
"note_size": "Notengröße",
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
"calculate": "berechnen",
"subtree_size": "(Zweiggröße: {{size}} in {{count}} Notizen)",
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
"title": "Notizinfo",
"mime": "MIME Typ",
"show_similar_notes": "Zeige ähnliche Notizen"
@@ -871,7 +871,7 @@
"owned_attributes": "Eigene Attribute"
},
"promoted_attributes": {
"promoted_attributes": "Hervorgehobene Attribute",
"promoted_attributes": "Übergebene Attribute",
"url_placeholder": "http://website...",
"open_external_link": "Externen Link öffnen",
"unknown_label_type": "Unbekannter Labeltyp „{{type}}“",
@@ -1115,7 +1115,7 @@
"vacuum_database": {
"title": "Datenbank aufräumen",
"description": "Dadurch wird die Datenbank neu erstellt, was normalerweise zu einer kleineren Datenbankdatei führt. Es werden keine Daten tatsächlich geändert.",
"button_text": "Datenbank aufräumen",
"button_text": "Vakuumdatenbank",
"vacuuming_database": "Datenbank wird geleert...",
"database_vacuumed": "Die Datenbank wurde geleert"
},
@@ -1156,7 +1156,7 @@
},
"ribbon": {
"widgets": "Multifunktionsleisten-Widgets",
"promoted_attributes_message": "Die „Hervorgehobene Attribute“-Leiste wird automatisch geöffnet, wenn in der Notiz hervorgehobene Attribute vorhanden sind",
"promoted_attributes_message": "Die Multifunktionsleisten-Registerkarte „Heraufgestufte Attribute“ wird automatisch geöffnet, wenn in der Notiz heraufgestufte Attribute vorhanden sind",
"edited_notes_message": "Die Multifunktionsleisten-Registerkarte „Bearbeitete Notizen“ wird bei Tagesnotizen automatisch geöffnet"
},
"theme": {
@@ -1445,19 +1445,19 @@
"insert-note-after": "Notiz dahinter einfügen",
"insert-child-note": "Unternotiz einfügen",
"delete": "Löschen",
"search-in-subtree": "Im Zweig suchen",
"search-in-subtree": "Im Notizbaum suchen",
"hoist-note": "Notiz-Fokus setzen",
"unhoist-note": "Notiz-Fokus aufheben",
"edit-branch-prefix": "Zweig-Präfix bearbeiten",
"advanced": "Erweitert",
"expand-subtree": "Zweig aufklappen",
"collapse-subtree": "Zweig einklappen",
"expand-subtree": "Unterzweig aufklappen",
"collapse-subtree": "Notizbaum einklappen",
"sort-by": "Sortieren nach...",
"recent-changes-in-subtree": "Kürzliche Änderungen im Zweig",
"recent-changes-in-subtree": "Kürzliche Änderungen im Notizbaum",
"convert-to-attachment": "Als Anhang konvertieren",
"copy-note-path-to-clipboard": "Notiz-Pfad in die Zwischenablage kopieren",
"protect-subtree": "Zweig schützen",
"unprotect-subtree": "Zweig-Schutz aufheben",
"protect-subtree": "Notizbaum schützen",
"unprotect-subtree": "Notizenbaum-Schutz aufheben",
"copy-clone": "Kopieren / Klonen",
"clone-to": "Klonen nach...",
"cut": "Ausschneiden",
@@ -1474,12 +1474,12 @@
"archive": "Archiviere",
"unarchive": "Entarchivieren",
"open-in-a-new-window": "In neuem Fenster öffnen",
"hide-subtree": "Zweig ausblenden",
"show-subtree": "Zweig anzeigen"
"hide-subtree": "Teilbaum ausblenden",
"show-subtree": "Teilbaum anzeigen"
},
"shared_info": {
"shared_publicly": "Diese Notiz ist öffentlich freigegeben über {{- link}}.",
"shared_locally": "Diese Notiz ist lokal freigegeben über {{- link}}.",
"shared_publicly": "Diese Notiz ist öffentlich geteilt auf {{- link}}.",
"shared_locally": "Diese Notiz ist lokal geteilt auf {{- link}}.",
"help_link": "Für Hilfe besuche <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
},
"note_types": {
@@ -1514,10 +1514,10 @@
"toggle-off-hint": "Notiz ist geschützt, klicken, um den Schutz aufzuheben"
},
"shared_switch": {
"shared": "Freigegeben",
"toggle-on-title": "Notiz freigeben",
"shared": "Teilen",
"toggle-on-title": "Notiz teilen",
"toggle-off-title": "Notiz-Freigabe aufheben",
"shared-branch": "Diese Notiz existiert nur als freigegebene Notiz, das Aufheben der Freigabe würde sie löschen. Möchtest du fortfahren und die Notiz damit löschen?",
"shared-branch": "Diese Notiz existiert nur als geteilte Notiz, das Aufheben der Freigabe würde sie löschen. Möchtest du fortfahren und die Notiz damit löschen?",
"inherited": "Die Notiz kann hier nicht von der Freigabe entfernt werden, da sie über Vererbung von einer übergeordneten Notiz geteilt wird."
},
"template_switch": {
@@ -1566,15 +1566,15 @@
"unhoist": "Fokus verlassen",
"toggle-sidebar": "Seitenleiste ein-/ausblenden",
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig.",
"clone-indicator-tooltip": "Diese Notiz hat {{- count}} übergeordnete Knoten: {{- parents}}",
"clone-indicator-tooltip-single": "Diese Notiz ist geklont (1 weitere Quelle: {{- parent}})",
"shared-indicator-tooltip": "Diese Notiz ist öffentlich freigegeben",
"shared-indicator-tooltip-with-url": "Diese Notiz ist öffentlich freigegeben unter: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} untergeordnete Notiz, die im Baum ausgeblendet ist",
"subtree-hidden-tooltip_other": "{{count}} untergeordnete Notizen, die im Baum ausgeblendet sind",
"clone-indicator-tooltip": "Diese Notiz hat {{- count}} Elterknoten: {{- parents}}",
"clone-indicator-tooltip-single": "Diese Notiz ist geklont (1 weiterer Elternknoten: {{- parent}})",
"shared-indicator-tooltip": "Diese Notiz ist öffentlich einsehbar",
"shared-indicator-tooltip-with-url": "Diese Notiz ist unter {{- url}} öffentlich einsehbar",
"subtree-hidden-tooltip_one": "{{count}} Unterknoten, der im Baum ausgeblendet ist",
"subtree-hidden-tooltip_other": "{{count}} Unterknoten, die im Baum ausgeblendet sind",
"subtree-hidden-moved-title": "Zu {{title}} hinzugefügt",
"subtree-hidden-moved-description-collection": "Diese Sammlung blendet ihre Unternotizen im Baum aus.",
"subtree-hidden-moved-description-other": "Untergeordnete Notizen sind im Baum für diese Notiz ausgeblendet."
"subtree-hidden-moved-description-collection": "Diese Sammlung blendet ihre Unternotizem im Baum aus.",
"subtree-hidden-moved-description-other": "Diese Sammlung blendet ihre Unterknoten im Baum aus."
},
"title_bar_buttons": {
"window-on-top": "Dieses Fenster immer oben halten"
@@ -1586,8 +1586,8 @@
"print_report_title": "Druckreport",
"print_report_collection_details_button": "Details anzeigen",
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt oder geschützt sind."
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt ist oder geschützt ist.",
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt sind oder geschützt sind."
},
"note_title": {
"placeholder": "Titel der Notiz hier eingeben…",
@@ -1751,8 +1751,8 @@
"desktop-application": "Desktop Anwendung",
"native-title-bar": "Native Anwendungsleiste",
"native-title-bar-description": "In Windows und macOS, sorgt das Deaktivieren der nativen Anwendungsleiste für ein kompakteres Aussehen. Unter Linux, sorgt das Aktivieren der nativen Anwendungsleiste für eine bessere Integration mit anderen Teilen des Systems.",
"background-effects": "Hintergrundeffekte aktivieren",
"background-effects-description": "Fügt einen unscharfen, stylischen Hintergrund in das Anwendungsfenstern ein. Dies erzeugt Tiefe und ein modernes Auftreten. \"Native Titelleiste\" muss deaktiviert sein.",
"background-effects": "Hintergrundeffekte aktivieren (nur Windows 11)",
"background-effects-description": "Der Mica Effekt fügt einen unscharfen, stylischen Hintergrund in Anwendungsfenstern ein. Dieser erzeugt Tiefe und ein modernes Auftreten. \"Native Titelleiste\" muss deaktiviert sein.",
"restart-app-button": "Anwendung neustarten um Änderungen anzuwenden",
"zoom-factor": "Zoomfaktor"
},
@@ -2000,7 +2000,7 @@
"check_share_root": "Status des Freigabe-Roots prüfen",
"share_root_found": "Freigabe-Root-Notiz '{{noteTitle}}' ist bereit",
"share_root_not_found": "Keine Notiz mit #shareRoot Label gefunden",
"share_root_not_shared": "Notiz '{{noteTitle}}' hat das #shareRoot Label, wurde jedoch noch nicht freigegeben"
"share_root_not_shared": "Notiz '{{noteTitle}}' hat das #shareRoot Label, wurde jedoch noch nicht geteilt"
},
"tasks": {
"due": {
@@ -2118,8 +2118,8 @@
"show_attachments_description": "Notizanhänge anzeigen",
"search_notes_title": "Suche Notiz",
"search_notes_description": "Öffne erweiterte Suche",
"search_subtree_title": "Im Zweig suchen",
"search_subtree_description": "Im aktuellen Zweig suchen",
"search_subtree_title": "Im Unterzweig suchen",
"search_subtree_description": "Im aktuellen Unterzweig suchen",
"search_history_title": "Zeige Suchhistorie",
"search_history_description": "Zeige vorherige Suchen",
"configure_launch_bar_title": "Startleiste anpassen",
@@ -2133,7 +2133,7 @@
"next_theme_message": "Es wird aktuell das alte Design verwendet. Möchten Sie das neue Design ausprobieren?",
"next_theme_button": "Teste das neue Design",
"background_effects_title": "Hintergrundeffekte sind jetzt zuverlässig nutzbar",
"background_effects_message": "Auf Windows- und macOS-Geräten sind die Hintergrundeffekte nun stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird.",
"background_effects_message": "Auf Windows-Geräten sind die Hintergrundeffekte nun vollständig stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird. Diese Technik wird auch in anderen Anwendungen wie dem Windows-Explorer eingesetzt.",
"background_effects_button": "Aktiviere Hintergrundeffekte",
"dismiss": "Ablehnen",
"new_layout_title": "Neues Layout",
@@ -2188,9 +2188,9 @@
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern."
},
"server": {
"unknown_http_error_title": "Kommunikationsfehler mit dem Server",
"unknown_http_error_title": "Bei der Kommunikation mit dem Server ist ein Fehler aufgetreten",
"unknown_http_error_content": "Statuscode: {{statusCode}}\nURL: {{method}} {{url}}\nNachricht: {{message}}",
"traefik_blocks_requests": "Der Traefik Reverse-Proxy hat eine Änderung erfahren, welches die Kommunikation mit dem Server beeinflusst."
"traefik_blocks_requests": "Der Traefik Reverse-Proxy hat ein fatales Update bekommen, welche die Kommunikation mit dem Server stört."
},
"tab_history_navigation_buttons": {
"go-back": "Zur vorherigen Notiz zurück kehren",
@@ -2205,30 +2205,30 @@
"empty_hide_archived_notes": "Archivierte Notizen ausblenden"
},
"breadcrumb_badges": {
"read_only_explicit": "Schreibgeschützt",
"read_only_explicit_description": "Diese Notiz wurde händisch schreibgeschützt.\nKlicke hier um sie temporär zu bearbeiten.",
"read_only_auto": "Automatisch schreibgeschützt",
"read_only_auto_description": "Diese Notiz wurde automatisch aus Leistungsgründen als schreibgeschützt markiert. Dieses automatische Limit kann in den Einstellungen angepasst werden.\n\nKlicke hier, um sie temporär zu bearbeiten.",
"read_only_explicit": "Nicht Änderbar",
"read_only_explicit_description": "Diese Notiz wurde händisch als nicht änderbar markiert.\nKlicke hier um sie temporär zu bearbeiten.",
"read_only_auto": "Automatisch nicht änderbar",
"read_only_auto_description": "Diese Notiz wurde automatisch aus Leistungsgründen als nicht änderbar markiert. Dieses automatische Limit kann in den Einstellungen angepasst werden.\n\nKlicke hier, um sie temporär zu bearbeiten.",
"read_only_temporarily_disabled": "Temporär bearbeitbar",
"read_only_temporarily_disabled_description": "Diese Notiz ist aktuell bearbeitbar, ist aber normalerweise schreibgeschützt. Sobald du zu einer anderen Notiz navigierst wird diese wieder schreibgeschützt.\n\nKlicke hier, um die Notiz wieder schreibgeschützt zu machen.",
"shared_publicly": "Öffentlich freigegeben",
"shared_locally": "Lokal freigegeben",
"read_only_temporarily_disabled_description": "Diese Notiz ist aktuell bearbeitbar, ist aber normalerweise nicht änderbar. Sobald du zu einer anderen Notiz navigierst, kehrt diese Notiz in ihren Normalzustand zurück.\n\nKlicke hier, um die Notiz wieder nicht änderbar zu machen.",
"shared_publicly": "Öffentlich geteilt",
"shared_locally": "Lokal geteilt",
"shared_copy_to_clipboard": "Link in die Zwischenablage kopieren",
"shared_open_in_browser": "Link im Browser öffnen",
"shared_unshare": "Freigabe aufheben",
"shared_open_in_browser": "Link öffnen",
"shared_unshare": "Teilen aufheben",
"clipped_note": "Internetschnellverweis",
"clipped_note_description": "Diese Notiz wurde von {{url}} übernommen.\n\nKlicke hier, um zur Quelle zu gehen.",
"clipped_note_description": "Diese Notiz wurde von {{url}} übernommen.\n\nKlicke hier, um zum Ursprung zu gehen.",
"execute_script": "Skript ausführen",
"execute_script_description": "Diese Notiz ist eine Skriptnotiz. Klicke hier, um das Skript auszuführen.",
"execute_sql": "SQL ausführen",
"execute_sql_description": "Diese Notiz ist eine SQL-Notiz. Klicke hier, um die SQL-Abfrage auszuführen.",
"save_status_saved": "Gespeichert",
"save_status_saving": "Speichere...",
"save_status_saving": "Speichern...",
"save_status_unsaved": "Nicht gespeichert",
"save_status_error": "Speichern fehlgeschlagen",
"save_status_saving_tooltip": "Änderungen werden gespeichert.",
"save_status_unsaved_tooltip": "Es gibt ungespeicherte Änderungen, welche gleich automatisch gespeichert werden.",
"save_status_error_tooltip": "Beim speichern der Notiz ist ein Fehler aufgetreten. Wenn möglich, versuche die Notiz woandershin zu kopieren und die Anwendung neu zu laden."
"save_status_error_tooltip": "Beim speichern der Notiz ist ein Fehler aufgetreten. Wenn möglich, versuche die Notiz woandershin zu kopieren und die Applikation neu zu laden."
},
"status_bar": {
"language_title": "Inhaltssprache ändern",
@@ -2241,7 +2241,7 @@
"attachments_other": "{{count}} Anhänge",
"attachments_title_one": "Anhang in einem neuen Tab öffnen",
"attachments_title_other": "Anhänge in einem neuen Tab öffnen",
"attributes_one": "{{count}} Attribut",
"attributes_one": "{{count}} Attribute",
"attributes_other": "{{count}} Attribute",
"attributes_title": "Eigene und geerbte Attribute",
"note_paths_one": "{{count}} Pfad",
@@ -2254,9 +2254,9 @@
},
"right_pane": {
"empty_message": "Für diese Notiz gibt es nichts anzuzeigen",
"empty_button": "Leiste ausblenden",
"toggle": "Rechte Leiste umschalten",
"custom_widget_go_to_source": "Zum Quellcode"
"empty_button": "Anzeige ausblenden",
"toggle": "Rechte Anzeige umschalten",
"custom_widget_go_to_source": "Zum Ursprungscode"
},
"pdf": {
"attachments_one": "{{count}} Anhang",
@@ -2266,9 +2266,6 @@
"pages_one": "{{count}} Seite",
"pages_other": "{{count}} Seiten",
"pages_alt": "Seite {{pageNumber}}",
"pages_loading": "Lädt..."
},
"platform_indicator": {
"available_on": "Verfügbar auf {{platform}}"
"pages_loading": "Laden..."
}
}

View File

@@ -1958,8 +1958,8 @@
"desktop-application": "Desktop Application",
"native-title-bar": "Native title bar",
"native-title-bar-description": "For Windows and macOS, keeping the native title bar off makes the application look more compact. On Linux, keeping the native title bar on integrates better with the rest of the system.",
"background-effects": "Enable background effects",
"background-effects-description": "Adds a blurred, stylish background to app windows, creating depth and a modern look. \"Native title bar\" must be disabled.",
"background-effects": "Enable background effects (Windows 11 only)",
"background-effects-description": "The Mica effect adds a blurred, stylish background to app windows, creating depth and a modern look. \"Native title bar\" must be disabled.",
"restart-app-button": "Restart the application to view the changes",
"zoom-factor": "Zoom factor"
},
@@ -1977,7 +1977,6 @@
},
"geo-map": {
"create-child-note-title": "Create a new child note and add it to the map",
"create-child-note-text": "Add marker",
"create-child-note-instruction": "Click on the map to create a new note at that location or press Escape to dismiss.",
"unable-to-load-map": "Unable to load map."
},
@@ -2153,7 +2152,7 @@
"next_theme_message": "You are currently using the legacy theme, would you like to try the new theme?",
"next_theme_button": "Try the new theme",
"background_effects_title": "Background effects are now stable",
"background_effects_message": "On Windows and macOS devices, background effects are now stable. The background effects adds a touch of color to the user interface by blurring the background behind it.",
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
"background_effects_button": "Enable background effects",
"new_layout_title": "New layout",
"new_layout_message": "Weve introduced a modernized layout for Trilium. The ribbon has been removed and seamlessly integrated into the main interface, with a new status bar and expandable sections (such as promoted attributes) taking over key functions.\n\nThe new layout is enabled by default, and can be temporarily disabled via Options → Appearance.",
@@ -2268,13 +2267,5 @@
"pages_other": "{{count}} pages",
"pages_alt": "Page {{pageNumber}}",
"pages_loading": "Loading..."
},
"platform_indicator": {
"available_on": "Available on {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} tab",
"title_other": "{{count}} tabs",
"more_options": "More options"
}
}

View File

@@ -799,11 +799,11 @@
"board": "Tablero",
"include_archived_notes": "Mostrar notas archivadas",
"presentation": "Presentación",
"expand_tooltip": "Expande las subnotas inmediatas de esta colección (un nivel). Para más opciones, pulsa la flecha a la derecha.",
"expand_tooltip": "Expande las notas hijas inmediatas de esta colección (un nivel). Para más opciones, pulsa la flecha a la derecha.",
"expand_first_level": "Expandir hijos inmediatos",
"expand_nth_level": "Expandir {{depth}} niveles",
"expand_all_levels": "Expandir todos los niveles",
"hide_child_notes": "Ocultar subnotas en el árbol"
"hide_child_notes": "Ocultar notas hijas en el árbol"
},
"edited_notes": {
"no_edited_notes_found": "Aún no hay notas editadas en este día...",
@@ -849,8 +849,7 @@
"calculate": "calcular",
"subtree_size": "(tamaño del subárbol: {{size}} en {{count}} notas)",
"title": "Información de nota",
"mime": "Tipo MIME",
"show_similar_notes": "Mostrar notas similares"
"mime": "Tipo MIME"
},
"note_map": {
"open_full": "Ampliar al máximo",
@@ -913,8 +912,7 @@
"search_parameters": "Parámetros de búsqueda",
"unknown_search_option": "Opción de búsqueda desconocida {{searchOptionName}}",
"search_note_saved": "La nota de búsqueda se ha guardado en {{- notePathTitle}}",
"actions_executed": "Las acciones han sido ejecutadas.",
"view_options": "Ver opciones:"
"actions_executed": "Las acciones han sido ejecutadas."
},
"similar_notes": {
"title": "Notas similares",
@@ -1017,13 +1015,7 @@
},
"editable_text": {
"placeholder": "Escribe aquí el contenido de tu nota...",
"auto-detect-language": "Detectado automáticamente",
"editor_crashed_title": "El editor de texto ha dejado de responder",
"editor_crashed_content": "Su contenido ha sido recuperado con éxito, pero puede que algunos de sus cambios más recientes no se hayan guardado.",
"editor_crashed_details_button": "Ver más detalles...",
"editor_crashed_details_intro": "Si experimenta este error varias veces, considere informarlo en GitHub adjuntando la siguiente información.",
"editor_crashed_details_title": "Información técnica",
"keeps-crashing": "El componente de edición sigue fallando. Por favor, intente reiniciar Trilium. Si el problema persiste, considere crear un informe de fallos."
"auto-detect-language": "Detectado automáticamente"
},
"empty": {
"open_note_instruction": "Abra una nota escribiendo el título de la nota en la entrada a continuación o elija una nota en el árbol.",
@@ -1339,8 +1331,8 @@
"code_mime_types": {
"title": "Tipos MIME disponibles en el menú desplegable",
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
"tooltip_code_block_syntax": "Bloques de código en Notas de texto",
"tooltip_code_note_syntax": "Notas de código"
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
"tooltip_code_note_syntax": "Notas de Código"
},
"vim_key_bindings": {
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
@@ -1417,16 +1409,16 @@
"markdown": "Estilo Markdown"
},
"highlights_list": {
"title": "Lista de puntos destacados",
"description": "Puede personalizar la lista de puntos destacados que se muestra en el panel derecho:",
"title": "Lista de aspectos destacados",
"description": "Puede personalizar la lista de aspectos destacados que se muestra en el panel derecho:",
"bold": "Texto en negrita",
"italic": "Texto en cursiva",
"underline": "Texto subrayado",
"color": "Texto con color",
"bg_color": "Texto con color de fondo",
"visibility_title": "Visibilidad de la lista de puntos destacados",
"visibility_description": "Puede ocultar el widget de puntos destacados por nota agregando la etiqueta #hideHighlightWidget.",
"shortcut_info": "Puede configurar un método abreviado de teclado para alternar rápidamente el panel derecho (incluidos los puntos destacados) en Opciones -> Atajos (nombre 'toggleRightPane')."
"visibility_title": "Visibilidad de la lista de aspectos destacados",
"visibility_description": "Puede ocultar el widget de aspectos destacados por nota agregando una etiqueta #hideHighlightWidget.",
"shortcut_info": "Puede configurar un método abreviado de teclado para alternar rápidamente el panel derecho (incluidos los aspectos destacados) en Opciones -> Atajos (nombre 'toggleRightPane')."
},
"table_of_contents": {
"title": "Tabla de contenido",
@@ -1614,7 +1606,7 @@
},
"bookmark_switch": {
"bookmark": "Marcador",
"bookmark_this_note": "Agregar esta nota a marcadores en el panel lateral izquierdo",
"bookmark_this_note": "Añadir esta nota a marcadores en el panel lateral izquierdo",
"remove_bookmark": "Eliminar marcador"
},
"editability_select": {
@@ -1662,10 +1654,7 @@
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
"open-in-popup": "Edición rápida",
"archive": "Archivar",
"unarchive": "Desarchivar",
"open-in-a-new-window": "Abrir en una nueva ventana",
"hide-subtree": "Ocultar subárbol",
"show-subtree": "Mostrar subárbol"
"unarchive": "Desarchivar"
},
"shared_info": {
"shared_publicly": "Esta nota está compartida públicamente en {{- link}}.",
@@ -1726,13 +1715,7 @@
},
"highlights_list_2": {
"title": "Lista de destacados",
"options": "Opciones",
"title_with_count_one": "{{count}} punto destacado",
"title_with_count_many": "{{count}} puntos destacados",
"title_with_count_other": "{{count}} puntos destacados",
"modal_title": "Configurar la lista de puntos destacados",
"menu_configure": "Configurar la lista de puntos destacados...",
"no_highlights": "Ningún punto destacado encontrado."
"options": "Opciones"
},
"quick-search": {
"placeholder": "Búsqueda rápida",
@@ -1755,18 +1738,7 @@
"refresh-saved-search-results": "Refrescar resultados de búsqueda guardados",
"create-child-note": "Crear subnota",
"unhoist": "Desanclar",
"toggle-sidebar": "Alternar barra lateral",
"dropping-not-allowed": "No está permitido soltar notas en esta ubicación.",
"clone-indicator-tooltip": "Esta nota tiene {{- count}} padres: {{- parents}}",
"clone-indicator-tooltip-single": "Esta nota está clonada (1 padre adicional: {{- parent}})",
"shared-indicator-tooltip": "Esta nota está compartida públicamente",
"shared-indicator-tooltip-with-url": "Esta nota está compartida públicamente en: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} subnota que está oculta del árbol",
"subtree-hidden-tooltip_many": "{{count}} subnotas que están ocultas del árbol",
"subtree-hidden-tooltip_other": "{{count}} subnotas que están ocultas del árbol",
"subtree-hidden-moved-title": "Agregado a {{title}}",
"subtree-hidden-moved-description-collection": "Esta colección oculta sus subnotas en el árbol.",
"subtree-hidden-moved-description-other": "Las subnotas están ocultas en el árbol para esta nota."
"toggle-sidebar": "Alternar barra lateral"
},
"title_bar_buttons": {
"window-on-top": "Mantener esta ventana en la parte superior"
@@ -1777,21 +1749,10 @@
"printing_pdf": "Exportando a PDF en curso..",
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
"print_report_title": "Imprimir informe",
"print_report_collection_details_button": "Ver detalles",
"print_report_collection_details_ignored_notes": "Notas ignoradas"
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
},
"note_title": {
"placeholder": "escriba el título de la nota aquí...",
"created_on": "Creado en <Value />",
"last_modified": "Modificado en <Value />",
"note_type_switcher_label": "Cambiar de {{type}} a:",
"note_type_switcher_others": "Otro tipo de nota",
"note_type_switcher_templates": "Plantilla",
"note_type_switcher_collection": "Colección",
"edited_notes": "Notas editadas en este día",
"promoted_attributes": "Atributos promovidos"
"placeholder": "escriba el título de la nota aquí..."
},
"search_result": {
"no_notes_found": "No se han encontrado notas para los parámetros de búsqueda dados.",
@@ -1801,11 +1762,7 @@
"configure_launchbar": "Configurar barra de lanzamiento"
},
"sql_result": {
"no_rows": "No se han devuelto filas para esta consulta",
"not_executed": "La consulta aún no ha sido ejecutada.",
"failed": "La ejecución de la consulta SQL ha fallado",
"statement_result": "Resultado de declaración",
"execute_now": "Ejecutar ahora"
"no_rows": "No se han devuelto filas para esta consulta"
},
"sql_table_schemas": {
"tables": "Tablas"
@@ -1824,8 +1781,7 @@
},
"toc": {
"table_of_contents": "Tabla de contenido",
"options": "Opciones",
"no_headings": "Sin encabezados."
"options": "Opciones"
},
"watched_file_update_status": {
"file_last_modified": "Archivo <code class=\"file-path\"></code> ha sido modificado por última vez en<span class=\"file-last-modified\"></span>.",
@@ -1937,15 +1893,14 @@
"open_note_in_new_tab": "Abrir nota en una pestaña nueva",
"open_note_in_new_split": "Abrir nota en una nueva división",
"open_note_in_new_window": "Abrir nota en una nueva ventana",
"open_note_in_popup": "Edición rápida",
"open_note_in_other_split": "Abrir nota en la otra división"
"open_note_in_popup": "Edición rápida"
},
"electron_integration": {
"desktop-application": "Aplicación de escritorio",
"native-title-bar": "Barra de título nativa",
"native-title-bar-description": "Para Windows y macOS, quitar la barra de título nativa hace que la aplicación se vea más compacta. En Linux, mantener la barra de título nativa hace que se integre mejor con el resto del sistema.",
"background-effects": "Habilitar efectos de fondo",
"background-effects-description": "Agrega un fondo borroso y elegante a las ventanas de la aplicación, creando profundidad y un aspecto moderno. \"Título nativo de la barra\" debe deshabilitarse.",
"background-effects": "Habilitar efectos de fondo (sólo en Windows 11)",
"background-effects-description": "El efecto Mica agrega un fondo borroso y elegante a las ventanas de la aplicación, creando profundidad y un aspecto moderno. \"Título nativo de la barra\" debe deshabilitarse.",
"restart-app-button": "Reiniciar la aplicación para ver los cambios",
"zoom-factor": "Factor de zoom"
},
@@ -1964,8 +1919,7 @@
"geo-map": {
"create-child-note-title": "Crear una nueva subnota y agregarla al mapa",
"create-child-note-instruction": "Dé clic en el mapa para crear una nueva nota en esa ubicación o presione Escape para cancelar.",
"unable-to-load-map": "No se puede cargar el mapa.",
"create-child-note-text": "Agregar marcador"
"unable-to-load-map": "No se puede cargar el mapa."
},
"geo-map-context": {
"open-location": "Abrir ubicación",
@@ -2008,11 +1962,10 @@
},
"note_language": {
"not_set": "Idioma no establecido",
"configure-languages": "Configurar idiomas...",
"help-on-languages": "Ayuda en idiomas de contenido..."
"configure-languages": "Configurar idiomas..."
},
"content_language": {
"title": "Idiomas de contenido",
"title": "Contenido de idiomas",
"description": "Seleccione uno o más idiomas que deben aparecer en la selección del idioma en la sección Propiedades Básicas de una nota de texto de solo lectura o editable. Esto permitirá características tales como corrección de ortografía o soporte de derecha a izquierda."
},
"switch_layout_button": {
@@ -2027,8 +1980,7 @@
"button_title": "Exportar diagrama como PNG"
},
"svg": {
"export_to_png": "El diagrama no pudo ser exportado a PNG.",
"export_to_svg": "El diagrama no pudo ser exportado a SVG."
"export_to_png": "El diagrama no pudo ser exportado a PNG."
},
"code_theme": {
"title": "Apariencia",
@@ -2131,12 +2083,9 @@
"next_theme_message": "Estás usando actualmente el tema heredado. ¿Te gustaría probar el nuevo tema?",
"next_theme_button": "Prueba el nuevo tema",
"background_effects_title": "Los efectos de fondo son ahora estables",
"background_effects_message": "En los dispositivos Windows y macOS, los efectos de fondo ya son estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás.",
"background_effects_message": "En los dispositivos Windows, los efectos de fondo ya son totalmente estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás. Esta técnica también se utiliza en otras aplicaciones como el Explorador de Windows.",
"background_effects_button": "Activar efectos de fondo",
"dismiss": "Desestimar",
"new_layout_title": "Nuevo diseño",
"new_layout_message": "Hemos introducido un diseño modernizado para Trilium. La cinta se ha eliminado y se ha integrado perfectamente en la interfaz principal, con una nueva barra de estado y secciones ampliables (como los atributos promovidos) que tienen funciones clave.\n\nEl nuevo diseño está habilitado por defecto, y puede ser deshabilitado temporalmente a través de Opciones → Apariencia.",
"new_layout_button": "Más información"
"dismiss": "Desestimar"
},
"ui-performance": {
"title": "Rendimiento",
@@ -2151,10 +2100,7 @@
},
"settings_appearance": {
"related_code_blocks": "Esquema de colores para bloques de código en notas de texto",
"related_code_notes": "Esquema de colores para notas de código",
"ui": "Interfaz de usuario",
"ui_old_layout": "Antiguo diseño",
"ui_new_layout": "Nuevo diseño"
"related_code_notes": "Esquema de colores para notas de código"
},
"units": {
"percentage": "%"
@@ -2202,12 +2148,7 @@
"attributes_other": "{{count}} atributos",
"note_paths_one": "{{count}} ruta",
"note_paths_many": "{{count}} rutas",
"note_paths_other": "{{count}} rutas",
"language_title": "Cambiar el idioma del contenido",
"note_info_title": "Ver información de la nota (p. e., fechas, tamaño de la nota)",
"attributes_title": "Atributos propios y atributos heredados",
"note_paths_title": "Rutas de nota",
"code_note_switcher": "Cambiar modo de idioma"
"note_paths_other": "{{count}} rutas"
},
"pdf": {
"attachments_one": "{{count}} adjunto",
@@ -2218,72 +2159,6 @@
"layers_other": "{{count}} capas",
"pages_one": "{{count}} página",
"pages_many": "{{count}} páginas",
"pages_other": "{{count}} páginas",
"pages_alt": "Página {{pageNumber}}",
"pages_loading": "Cargando..."
},
"experimental_features": {
"title": "Opciones experimentales",
"disclaimer": "Estas opciones son experimentales y pueden causar inestabilidad. Úselas con precaución.",
"new_layout_name": "Nuevo diseño",
"new_layout_description": "Pruebe el nuevo diseño para tener un aspecto más moderno y usabilidad mejorada. Sujeto a grandes cambios en las próximas versiones."
},
"popup-editor": {
"maximize": "Cambiar a editor completo"
},
"server": {
"unknown_http_error_title": "Error de comunicación con el servidor",
"unknown_http_error_content": "Código de estado: {{statusCode}}\nURL: {{method}} {{url}}\nMensaje: {{message}}",
"traefik_blocks_requests": "Si está usando el proxy inverso Traefik, este introdujo un cambio que afecta la comunicación con el servidor."
},
"tab_history_navigation_buttons": {
"go-back": "Volver a la nota anterior",
"go-forward": "Avanzar a la siguiente nota"
},
"breadcrumb": {
"hoisted_badge": "Anclada",
"hoisted_badge_title": "Desanclar",
"workspace_badge": "Espacio de trabajo",
"scroll_to_top_title": "Saltar al inicio de la nota",
"create_new_note": "Crear nueva subnota",
"empty_hide_archived_notes": "Ocultar notas archivadas"
},
"breadcrumb_badges": {
"read_only_explicit": "Sólo lectura",
"read_only_explicit_description": "Esta nota se ha fijado manualmente como sólo lectura.\nHaga clic para editarla temporalmente.",
"read_only_auto": "Sólo lectura automática",
"read_only_auto_description": "Esta nota se fijó automáticamente con el modo de sólo lectura por razones de rendimiento. Este límite automático es ajustable desde los ajustes.\n\nHaga clic para editarla temporalmente.",
"read_only_temporarily_disabled": "Temporalmente editable",
"read_only_temporarily_disabled_description": "Esta nota actualmente es editable, pero normalmente es de sólo lectura. La nota volverá a ser de sólo lectura tan pronto como navegue a otra nota.\n\nHaga clic para volver a habilitar el modo de sólo lectura.",
"shared_publicly": "Compartida públicamente",
"shared_locally": "Compartida localmente",
"shared_copy_to_clipboard": "Copiar enlace al portapapeles",
"shared_open_in_browser": "Abrir enlace en el navegador",
"shared_unshare": "Eliminar compartido",
"clipped_note_description": "Esta nota fue tomada originalmente de {{url}}.\n\nHaga clic para navegar a la página web de origen.",
"execute_script": "Ejecutar script",
"execute_script_description": "Esta nota es una nota de script. Haga clic para ejecutar el script.",
"execute_sql": "Ejecutar SQL",
"execute_sql_description": "Esta nota es una nota SQL. Haga clic para ejecutar la consulta SQL.",
"save_status_saved": "Guardado",
"save_status_saving": "Guardando...",
"save_status_unsaved": "Sin guardar",
"save_status_error": "Fallo al guardar",
"save_status_saving_tooltip": "Los cambios están siendo guardados.",
"save_status_unsaved_tooltip": "Hay cambios sin guardar. Se guardarán automáticamente en un momento.",
"save_status_error_tooltip": "Se produjo un error al guardar la nota. Si es posible, trate de copiar el contenido de la nota en otro lugar y recargar la aplicación.",
"clipped_note": "Clip web"
},
"attributes_panel": {
"title": "Atributos de nota"
},
"right_pane": {
"empty_message": "Nada que mostrar para esta nota",
"empty_button": "Ocultar el panel",
"toggle": "Alternar panel derecho",
"custom_widget_go_to_source": "Ir al código fuente"
},
"platform_indicator": {
"available_on": "Disponible en {{platform}}"
"pages_other": "{{count}} páginas"
}
}

View File

@@ -12,7 +12,7 @@
"toast": {
"critical-error": {
"title": "Eror kritikal",
"message": "Telah terjadi eror kritikal yang mencegah aplikasi klien untuk memulai:\n\n{{message}}\n\nHal ini kemungkinan besar disebabkan oleh skrip yang gagal secara tidak terduga. Coba jalankan aplikasi dalam mode aman dan atasi masalahnya."
"message": "Telah terjadi kesalahan kritis yang mencegah aplikasi klien untuk memulai:\n\n{{message}}\n\nHal ini kemungkinan besar disebabkan oleh skrip yang gagal secara tidak terduga. Coba jalankan aplikasi dalam mode aman dan atasi masalahnya."
},
"widget-error": {
"title": "Gagal menginisialisasi widget",
@@ -36,12 +36,7 @@
"add_link": {
"add_link": "Tambah tautan",
"help_on_links": "Bantuan pada tautan",
"note": "Catatan",
"search_note": "cari catatan berdasarkan nama",
"link_title_mirrors": "judul tautan mencerminkan judul catatan saat ini",
"link_title_arbitrary": "judul tautan dapat diubah secara bebas",
"link_title": "Judul tautan",
"button_add_link": "Tambah tautan"
"note": "Catatan"
},
"branch_prefix": {
"edit_branch_prefix_multiple": "Edit prefiks cabang untuk {{count}} cabang",
@@ -78,8 +73,5 @@
"erase_notes_warning": "Hapus catatan secara permanen (tidak bisa dikembalikan), termasuk semua duplikat. Aksi akan memaksa aplikasi untuk mengulang kembali.",
"notes_to_be_deleted": "Catatan-catatan berikut akan dihapuskan ({{notesCount}})",
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat)."
},
"clone_to": {
"clone_notes_to": "Duplikat catatan ke…"
}
}

View File

@@ -325,10 +325,7 @@
"apply-bulk-actions": "Applica azioni in blocco",
"converted-to-attachments": "{{count}} note sono state convertite in allegati.",
"convert-to-attachment-confirm": "Sei sicuro di voler convertire le note selezionate in allegati delle note principali? Questa operazione si applica solo alle note immagine, le altre note verranno ignorate.",
"open-in-popup": "Modifica rapida",
"open-in-a-new-window": "Apri in una nuova finestra",
"hide-subtree": "Nascondi sottostruttura",
"show-subtree": "Mostra sottoalbero"
"open-in-popup": "Modifica rapida"
},
"electron_context_menu": {
"cut": "Taglia",
@@ -1381,8 +1378,7 @@
"expand_tooltip": "Espande i figli diretti di questa raccolta (a un livello di profondità). Per ulteriori opzioni, premere la freccia a destra.",
"expand_first_level": "Espandi figli diretti",
"expand_nth_level": "Espandi {{depth}} livelli",
"expand_all_levels": "Espandi tutti i livelli",
"hide_child_notes": "Nascondi note secondarie nell'albero"
"expand_all_levels": "Espandi tutti i livelli"
},
"edited_notes": {
"no_edited_notes_found": "Nessuna nota modificata per questo giorno...",
@@ -1903,13 +1899,7 @@
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}",
"subtree-hidden-tooltip_one": "{{count}} nota secondaria nascosta dall'albero",
"subtree-hidden-tooltip_many": "{{count}} note secondarie nascoste dall'albero",
"subtree-hidden-tooltip_other": "{{count}} note secondarie nascoste dall'albero",
"subtree-hidden-moved-title": "Aggiunto a {{title}}",
"subtree-hidden-moved-description-collection": "Questa raccolta nasconde le sue note secondarie nell'albero.",
"subtree-hidden-moved-description-other": "Le note secondarie sono nascoste nell'albero di questa nota."
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
},
"title_bar_buttons": {
"window-on-top": "Mantieni la finestra in primo piano"
@@ -1944,11 +1934,7 @@
"configure_launchbar": "Configura Launchbar"
},
"sql_result": {
"no_rows": "Nessuna riga è stata restituita per questa query",
"not_executed": "La query non è stata ancora eseguita.",
"failed": "Esecuzione query SQL non riuscita",
"statement_result": "Risultato della dichiarazione",
"execute_now": "Esegui ora"
"no_rows": "Nessuna riga è stata restituita per questa query"
},
"watched_file_update_status": {
"file_last_modified": "Il file <code class=\"file-path\"></code> è stato modificato l'ultima volta il <span class=\"file-last-modified\"></span>.",

View File

@@ -1757,8 +1757,8 @@
"desktop-application": "デスクトップアプリケーション",
"native-title-bar": "ネイティブタイトルバー",
"native-title-bar-description": "WindowsとmacOSでは、ネイティブタイトルバーをオフにしておくと、アプリケーションがよりコンパクトに見えます。Linuxでは、ネイティブタイトルバーを表示したままの方が、他のシステムとの統一性が高まります。",
"background-effects": "背景効果を有効化",
"background-effects-description": "アプリウィンドウにぼかしの効いたスタイリッシュな背景を追加し、奥行きとモダンな外観を演出します。「ネイティブタイトルバー」を無効にする必要があります。",
"background-effects": "背景効果を有効化Windows 11のみ",
"background-effects-description": "Mica効果は、アプリウィンドウにぼかされたスタイリッシュな背景を追加し、奥行きとモダンな外観を演出します。「ネイティブタイトルバー」を無効にする必要があります。",
"restart-app-button": "アプリケーションを再起動して変更を反映",
"zoom-factor": "ズーム倍率"
},
@@ -2044,7 +2044,7 @@
"next_theme_message": "現在、レガシーテーマを使用しています。新しいテーマを試してみませんか?",
"next_theme_button": "新しいテーマを試す",
"background_effects_title": "背景効果が安定しました",
"background_effects_message": "WindowsおよびmacOSデバイスで、背景効果が安定しました。背景効果は、背景をぼかすことでユーザーインターフェースに彩りを添えます。",
"background_effects_message": "Windowsデバイスで、背景効果が完全に安定しました。背景効果は、背景をぼかすことでユーザーインターフェースに彩りを添えます。この技術は、Windowsエクスプローラーなどの他のアプリケーションでも使用されています。",
"background_effects_button": "背景効果を有効にする",
"dismiss": "却下",
"new_layout_title": "新しいレイアウト",
@@ -2253,8 +2253,5 @@
"pages_other": "{{count}} ページ",
"pages_alt": "ページ {{pageNumber}}",
"pages_loading": "読み込み中..."
},
"platform_indicator": {
"available_on": "{{platform}} で利用可能"
}
}

View File

@@ -54,6 +54,7 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
OpenTriliumApiDocsButton,
SaveToNoteButton,
RelationMapButtons,
GeoMapButtons,
CopyImageReferenceButton,
ExportImageButtons,
InAppHelpButton,
@@ -97,10 +98,10 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
/>;
}
function ToggleReadOnlyButton({ note, isDefaultViewMode }: FloatingButtonContext) {
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
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" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <FloatingButton
@@ -242,6 +243,17 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
);
}
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
const isEnabled = viewType === "geoMap" && !isReadOnly;
return isEnabled && (
<FloatingButton
icon="bx bx-plus-circle"
text={t("geo-map.create-child-note-title")}
onClick={() => triggerEvent("geoMapCreateChildNote")}
/>
);
}
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
const isEnabled = (
@@ -293,7 +305,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
function InAppHelpButton({ note }: FloatingButtonContext) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = note.type !== "book" && !!helpUrl;
const isEnabled = !!helpUrl;
return isEnabled && (
<FloatingButton

View File

@@ -1,7 +1,7 @@
.note-list-widget {
min-height: 0;
max-width: var(--max-content-width); /* Inherited from .note-split */
overflow: auto;
contain: none !important;
}
@@ -11,6 +11,10 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
margin-inline: auto;
}
.note-list-widget .note-list {
padding-block: 10px;
}
.note-list-widget.full-height,
.note-list-widget.full-height .note-list-widget-content {
height: 100%;
@@ -21,12 +25,8 @@ body.prefers-centered-content .note-list-widget:not(.full-height) {
}
/* #region Pagination */
.note-list-pager {
font-size: 1rem;
span.current-page {
text-decoration: underline;
font-weight: bold;
}
.note-list-pager span.current-page {
text-decoration: underline;
font-weight: bold;
}
/* #endregion */
/* #endregion */

View File

@@ -2,8 +2,7 @@
position: relative;
height: 100%;
user-select: none;
display: flex;
flex-direction: column;
overflow-x: auto;
--card-font-size: 0.9em;
--card-line-height: 1.2;
@@ -20,10 +19,8 @@ body.mobile .board-view {
height: 100%;
display: flex;
gap: 1em;
padding-inline: 12px;
padding-block: 4px;
padding: 1em;
align-items: flex-start;
overflow-x: auto;
}
.board-view-container .board-column {
@@ -355,4 +352,4 @@ body.mobile .board-view-container .board-column {
font-size: 0.9em;
max-width: 200px;
word-wrap: break-word;
}
}

View File

@@ -1,23 +1,20 @@
import "./index.css";
import { createContext, TargetedKeyboardEvent } from "preact";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import toast from "../../../services/toast";
import CollectionProperties from "../../note_bars/CollectionProperties";
import FormTextArea from "../../react/FormTextArea";
import FormTextBox from "../../react/FormTextBox";
import { ViewModeProps } from "../interface";
import "./index.css";
import { ColumnMap, getBoardData } from "./data";
import { useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteAutocomplete from "../../react/NoteAutocomplete";
import { onWheelHorizontalScroll } from "../../widget_utils";
import { ViewModeProps } from "../interface";
import { t } from "../../../services/i18n";
import Api from "./api";
import BoardApi from "./api";
import FormTextBox from "../../react/FormTextBox";
import { createContext, TargetedKeyboardEvent } from "preact";
import { onWheelHorizontalScroll } from "../../widget_utils";
import Column from "./column";
import { ColumnMap, getBoardData } from "./data";
import BoardApi from "./api";
import FormTextArea from "../../react/FormTextArea";
import FNote from "../../../entities/fnote";
import NoteAutocomplete from "../../react/NoteAutocomplete";
import toast from "../../../services/toast";
export interface BoardViewData {
columns?: BoardColumnData[];
@@ -148,7 +145,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
const insertBefore = mouseX < columnMiddle;
// Calculate the target position
const targetIndex = insertBefore ? index : index + 1;
let targetIndex = insertBefore ? index : index + 1;
setColumnDropPosition(targetIndex);
}, [draggedColumn]);
@@ -162,14 +159,15 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
}, [draggedColumn, columnDropPosition, handleColumnDrop]);
return (
<div className="board-view">
<CollectionProperties note={parentNote} />
<div
className="board-view"
onWheel={onWheelHorizontalScroll}
>
<BoardViewContext.Provider value={boardViewContext}>
{byColumn && columns && <div
className="board-view-container"
onDragOver={handleColumnDragOver}
onDrop={handleContainerDrop}
onWheel={onWheelHorizontalScroll}
>
{columns.map((column, index) => (
<>
@@ -196,7 +194,7 @@ export default function BoardView({ note: parentNote, noteIds, viewConfig, saveC
</div>}
</BoardViewContext.Provider>
</div>
);
)
}
function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMode: boolean }) {
@@ -220,26 +218,26 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo
tabIndex={300}
>
{!isCreatingNewColumn
? <>
<Icon icon="bx bx-plus" />{" "}
{t("board_view.add-column")}
</>
: (
<TitleEditor
placeholder={t("board_view.add-column-placeholder")}
save={async (columnName) => {
const created = await api.addNewColumn(columnName);
if (!created) {
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
}
}}
dismiss={() => setIsCreatingNewColumn(false)}
isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
? <>
<Icon icon="bx bx-plus" />{" "}
{t("board_view.add-column")}
</>
: (
<TitleEditor
placeholder={t("board_view.add-column-placeholder")}
save={async (columnName) => {
const created = await api.addNewColumn(columnName);
if (!created) {
toast.showMessage(t("board_view.column-already-exists"), undefined, "bx bx-duplicate");
}
}}
dismiss={() => setIsCreatingNewColumn(false)}
isNewItem
mode={isInRelationMode ? "relation" : "normal"}
/>
)}
</div>
);
)
}
export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: {
@@ -304,26 +302,26 @@ export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, is
onBlur={onBlur}
/>
);
}
return (
<NoteAutocomplete
inputRef={inputRef}
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
} else {
return (
<NoteAutocomplete
inputRef={inputRef}
noteId={currentValue ?? ""}
opts={{
hideAllButtons: true,
allowCreatingNotes: true
}}
onKeyDown={(e) => {
if (e.key === "Escape") {
dismiss();
}
}}
onBlur={() => dismiss()}
noteIdChanged={(newValue) => {
save(newValue);
dismiss();
}
}}
onBlur={() => dismiss()}
noteIdChanged={(newValue) => {
save(newValue);
dismiss();
}}
/>
);
}}
/>
);
}
}

View File

@@ -21,16 +21,7 @@
outline: 0;
height: 100%;
user-select: none;
padding: 0;
@media (max-width: 991px) {
padding: 0;
th {
font-weight: normal;
font-size: 0.9em;
}
}
padding: 10px;
}
.calendar-view a,
@@ -52,7 +43,6 @@
--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;
}
.calendar-container .fc-list-sticky .fc-list-day > * {
@@ -69,43 +59,41 @@
overflow: hidden;
}
.calendar-view .collection-properties {
.center-container {
justify-content: center;
.title {
min-width: 150px;
font-size: 1.2em;
text-align: center;
}
}
@media (max-width: 991px) {
>div {
justify-content: flex-start;
}
.right-container {
flex-grow: 0;
}
.center-container {
.title {
flex-grow: 1;
min-width: 110px;
font-size: 0.95em;
}
}
}
/* #region Header */
.calendar-view .calendar-header {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.calendar-view .calendar-header .btn {
min-width: unset !important;
}
.calendar-view .calendar-header > .title {
flex-grow: 1;
font-size: 1.3rem;
font-weight: normal;
}
body.desktop:not(.zen) .calendar-view .calendar-header {
padding-block-start: 4px;
padding-inline-end: 5em;
}
.search-result-widget-content .calendar-view .calendar-header {
padding-inline-end: unset !important;
}
/* #endregion */
/* #region Events */
/*
* week, month, year views
*/
.calendar-container a.fc-event {
.calendar-container a.fc-event {
text-decoration: none;
}
@@ -138,7 +126,7 @@
.calendar-view a.fc-timegrid-event,
.calendar-view a.fc-daygrid-event {
--border-color: transparent;
border: 2px solid;
border-left-width: 4px;
border-color: var(--border-color) var(--border-color) var(--border-color)
@@ -187,7 +175,7 @@
opacity: .75;
}
/*
/*
* List view
*/
@@ -200,4 +188,4 @@
--fc-event-border-color: var(--custom-color);
}
/* #endregion */
/* #endregion */

View File

@@ -14,11 +14,8 @@ import dialog from "../../../services/dialog";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import { isMobile } from "../../../services/utils";
import CollectionProperties from "../../note_bars/CollectionProperties";
import ActionButton from "../../react/ActionButton";
import Button, { ButtonGroup } from "../../react/Button";
import Dropdown from "../../react/Dropdown";
import { FormListItem } from "../../react/FormList";
import { useNoteLabel, useNoteLabelBoolean, useResizeObserver, useSpacedUpdate, useTriliumEvent, useTriliumOption, useTriliumOptionInt } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
@@ -44,28 +41,24 @@ const CALENDAR_VIEWS = [
{
type: "timeGridWeek",
name: t("calendar.week"),
icon: "bx bx-calendar-week",
previousText: t("calendar.week_previous"),
nextText: t("calendar.week_next")
},
{
type: "dayGridMonth",
name: t("calendar.month"),
icon: "bx bx-calendar",
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
},
{
type: "multiMonthYear",
name: t("calendar.year"),
icon: "bx bx-layer",
previousText: t("calendar.year_previous"),
nextText: t("calendar.year_next")
},
{
type: "listMonth",
name: t("calendar.list"),
icon: "bx bx-list-ol",
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
}
@@ -147,7 +140,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
return (plugins &&
<div className="calendar-view" ref={containerRef} tabIndex={100}>
<CalendarCollectionProperties note={note} calendarRef={calendarRef} />
<CalendarHeader calendarRef={calendarRef} />
<Calendar
events={eventBuilder}
calendarRef={calendarRef}
@@ -176,67 +169,28 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
);
}
function CalendarCollectionProperties({ note, calendarRef }: {
note: FNote;
calendarRef: RefObject<FullCalendar>;
}) {
function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
const { title, viewType: currentViewType } = useOnDatesSet(calendarRef);
const currentViewData = CALENDAR_VIEWS.find(v => calendarRef.current && v.type === currentViewType);
const isMobileLocal = isMobile();
return (
<CollectionProperties
note={note}
centerChildren={<>
<ActionButton icon="bx bx-chevron-left" text={currentViewData?.previousText ?? ""} onClick={() => calendarRef.current?.prev()} />
<span className="title">{title}</span>
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} onClick={() => calendarRef.current?.next()} />
<Button text={t("calendar.today")} onClick={() => calendarRef.current?.today()} />
{isMobileLocal && <MobileCalendarViewSwitcher calendarRef={calendarRef} />}
</>}
rightChildren={<>
{!isMobileLocal && <DesktopCalendarViewSwitcher calendarRef={calendarRef} />}
</>}
/>
);
}
function DesktopCalendarViewSwitcher({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
const { viewType: currentViewType } = useOnDatesSet(calendarRef);
return (
<>
<div className="calendar-header">
<span className="title">{title}</span>
<ButtonGroup>
{CALENDAR_VIEWS.map(viewData => (
<Button
key={viewData.type}
text={viewData.name}
text={viewData.name.toLocaleLowerCase()}
className={currentViewType === viewData.type ? "active" : ""}
onClick={() => calendarRef.current?.changeView(viewData.type)}
/>
))}
</ButtonGroup>
</>
);
}
function MobileCalendarViewSwitcher({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
const { viewType: currentViewType } = useOnDatesSet(calendarRef);
const currentViewTypeData = CALENDAR_VIEWS.find(view => view.type === currentViewType);
return (
<Dropdown
text={currentViewTypeData?.name}
>
{CALENDAR_VIEWS.map(viewData => (
<FormListItem
key={viewData.type}
selected={currentViewType === viewData.type}
icon={viewData.icon}
onClick={() => calendarRef.current?.changeView(viewData.type)}
>{viewData.name}</FormListItem>
))}
</Dropdown>
<Button text={t("calendar.today").toLocaleLowerCase()} onClick={() => calendarRef.current?.today()} />
<ButtonGroup>
<ActionButton icon="bx bx-chevron-left" text={currentViewData?.previousText ?? ""} frame onClick={() => calendarRef.current?.prev()} />
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup>
</div>
);
}
@@ -263,18 +217,17 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
}
function useLocale() {
const [ locale ] = useTriliumOption("locale");
const [ formattingLocale ] = useTriliumOption("formattingLocale");
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
useEffect(() => {
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale] ?? LOCALE_MAPPINGS[locale];
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale];
if (correspondingLocale) {
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
} else {
setCalendarLocale(undefined);
}
}, [formattingLocale, locale]);
});
return calendarLocale;
}

View File

@@ -2,13 +2,6 @@
overflow: hidden;
position: relative;
height: 100%;
display: flex;
flex-direction: column;
> .collection-properties {
position: relative;
z-index: 2000;
}
}
.geo-map-container {

View File

@@ -1,29 +1,24 @@
import Map from "./map";
import "./index.css";
import { ViewModeProps } from "../interface";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import { divIcon, GPXOptions, LatLng, LeafletMouseEvent } from "leaflet";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import Marker, { GpxTrack } from "./marker";
import froca from "../../../services/froca";
import FNote from "../../../entities/fnote";
import markerIcon from "leaflet/dist/images/marker-icon.png";
import markerIconShadow from "leaflet/dist/images/marker-shadow.png";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../../components/app_context";
import FNote from "../../../entities/fnote";
import branches from "../../../services/branches";
import froca from "../../../services/froca";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import toast from "../../../services/toast";
import CollectionProperties from "../../note_bars/CollectionProperties";
import ActionButton from "../../react/ActionButton";
import { ButtonOrActionButton } from "../../react/Button";
import { useNoteBlob, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useNoteTreeDrag, useSpacedUpdate, useTriliumEvent } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import TouchBar, { TouchBarButton, TouchBarSlider } from "../../react/TouchBar";
import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
import toast from "../../../services/toast";
import { t } from "../../../services/i18n";
import server from "../../../services/server";
import branches from "../../../services/branches";
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSlider } from "../../react/TouchBar";
import { ParentComponent } from "../../react/react_utils";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
const DEFAULT_ZOOM = 2;
@@ -55,7 +50,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
}
}, 5000);
useEffect(() => { froca.getNotes(noteIds).then(setNotes); }, [ noteIds ]);
useEffect(() => { froca.getNotes(noteIds).then(setNotes) }, [ noteIds ]);
useEffect(() => {
if (!note) return;
@@ -65,7 +60,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
// Note creation.
useTriliumEvent("geoMapCreateChildNote", () => {
toast.showPersistent({
toast.showPersistent({
icon: "plus",
id: "geo-new-note",
title: "New note",
@@ -135,19 +130,6 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
return (
<div className={`geo-view ${state === State.NewNote ? "placing-note" : ""}`}>
<CollectionProperties
note={note}
rightChildren={<>
<ToggleReadOnlyButton note={note} />
<ButtonOrActionButton
icon="bx bx-plus"
text={t("geo-map.create-child-note-text")}
title={t("geo-map.create-child-note-title")}
triggerCommand="geoMapCreateChildNote"
disabled={isReadOnly}
/>
</>}
/>
{ coordinates !== undefined && zoom !== undefined && <Map
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
@@ -169,16 +151,6 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
);
}
function ToggleReadOnlyButton({ note }: { note: FNote }) {
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
return <ActionButton
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)}
/>;
}
function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) {
const mime = useNoteProperty(note, "mime");
const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE);
@@ -232,7 +204,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean
onDragged={editable ? onDragged : undefined}
onClick={!editable ? onClick : undefined}
onContextMenu={onContextMenu}
/>;
/>
}
function NoteGpxTrack({ note }: { note: FNote }) {
@@ -266,7 +238,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
color: note.getLabelValue("color") ?? "blue"
}
}), [ color, iconClass ]);
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />;
return xmlString && <GpxTrack gpxXmlString={xmlString} options={options} />
}
function buildIcon(bxIconClass: string, colorClass?: string, title?: string, noteIdLink?: string, archived?: boolean) {
@@ -320,5 +292,5 @@ function GeoMapTouchBar({ state, map }: { state: State, map: L.Map | null | unde
enabled={state === State.Normal}
/>
</TouchBar>
);
)
}

View File

@@ -7,8 +7,7 @@ import attribute_renderer from "../../../services/attribute_renderer";
import content_renderer from "../../../services/content_renderer";
import { t } from "../../../services/i18n";
import link from "../../../services/link";
import CollectionProperties from "../../note_bars/CollectionProperties";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean, useNoteProperty } from "../../react/hooks";
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean } from "../../react/hooks";
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
import { ViewModeProps } from "../interface";
@@ -20,18 +19,11 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const noteType = useNoteProperty(note, "type");
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
return (
<div class="note-list list-view">
<CollectionProperties
note={note}
centerChildren={<Pager {...pagination} />}
/>
{ noteIds.length > 0 && <div class="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<Pager {...pagination} />
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
@@ -53,18 +45,11 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const noteType = useNoteProperty(note, "type");
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
return (
<div class="note-list grid-view">
<CollectionProperties
note={note}
centerChildren={<Pager {...pagination} />}
/>
<div class="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<Pager {...pagination} />
<div class="note-list-container use-tn-links">
{pageNotes?.map(childNote => (
@@ -155,7 +140,7 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;

View File

@@ -1,7 +1,11 @@
.presentation-view {
display: flex;
flex-direction: column;
height: 100%;
.presentation-button-bar {
position: absolute;
top: 1em;
right: 1em;
.floating-buttons-children {
top: 0;
}
}
.presentation-container {

View File

@@ -1,21 +1,18 @@
import "./index.css";
import { RefObject } from "preact";
import { ViewModeMedia, ViewModeProps } from "../interface";
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import Reveal from "reveal.js";
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
import { openInCurrentNoteContext } from "../../../components/note_context";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import CollectionProperties from "../../note_bars/CollectionProperties";
import ActionButton from "../../react/ActionButton";
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import ShadowDom from "../../react/ShadowDom";
import { ViewModeMedia, ViewModeProps } from "../interface";
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
import slideCustomStylesheet from "./slidejs.css?raw";
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
import ShadowDom from "../../react/ShadowDom";
import ActionButton from "../../react/ActionButton";
import "./index.css";
import { RefObject } from "preact";
import { openInCurrentNoteContext } from "../../../components/note_context";
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
import { t } from "../../../services/i18n";
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
import FNote from "../../../entities/fnote";
export default function PresentationView({ note, noteIds, media, onReady, onProgressChanged }: ViewModeProps<{}>) {
const [ presentation, setPresentation ] = useState<PresentationModel>();
@@ -54,17 +51,14 @@ export default function PresentationView({ note, noteIds, media, onReady, onProg
if (media === "screen") {
return (
<div class="presentation-view">
<CollectionProperties
note={note}
rightChildren={<ButtonOverlay containerRef={containerRef} api={api} />}
/>
<>
<ShadowDom
className="presentation-container"
containerRef={containerRef}
>{content}</ShadowDom>
</div>
);
<ButtonOverlay containerRef={containerRef} api={api} />
</>
)
} else if (media === "print") {
// Printing needs a query parameter that is read by Reveal.js.
const url = new URL(window.location.href);
@@ -114,34 +108,42 @@ function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivE
}, [ api ]);
return (
<>
<ActionButton
icon="bx bx-edit"
text={t("presentation_view.edit-slide")}
onClick={e => {
const currentSlide = api?.getCurrentSlide();
const noteId = getNoteIdFromSlide(currentSlide);
<div className="presentation-button-bar">
<div className="floating-buttons-children">
<ActionButton
className="floating-button"
icon="bx bx-edit"
text={t("presentation_view.edit-slide")}
noIconActionClass
onClick={e => {
const currentSlide = api?.getCurrentSlide();
const noteId = getNoteIdFromSlide(currentSlide);
if (noteId) {
openInCurrentNoteContext(e, noteId);
}
}}
/>
if (noteId) {
openInCurrentNoteContext(e, noteId);
}
}}
/>
<ActionButton
icon="bx bx-grid-horizontal"
text={t("presentation_view.slide-overview")}
active={isOverviewActive}
onClick={() => api?.toggleOverview()}
/>
<ActionButton
className="floating-button"
icon="bx bx-grid-horizontal"
text={t("presentation_view.slide-overview")}
active={isOverviewActive}
noIconActionClass
onClick={() => api?.toggleOverview()}
/>
<ActionButton
icon="bx bx-fullscreen"
text={t("presentation_view.start-presentation")}
onClick={() => containerRef.current?.requestFullscreen()}
/>
</>
);
<ActionButton
className="floating-button"
icon="bx bx-fullscreen"
text={t("presentation_view.start-presentation")}
noIconActionClass
onClick={() => containerRef.current?.requestFullscreen()}
/>
</div>
</div>
)
}
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
@@ -177,7 +179,7 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
api.destroy();
setRevealApi(undefined);
setApi(undefined);
};
}
}, []);
useEffect(() => {
@@ -189,19 +191,19 @@ function Presentation({ presentation, setApi } : { presentation: PresentationMod
<div className="slides">
{presentation.slides?.map(slide => {
if (!slide.verticalSlides) {
return <Slide key={slide.noteId} slide={slide} />;
return <Slide key={slide.noteId} slide={slide} />
} else {
return (
<section>
<Slide key={slide.noteId} slide={slide} />
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
</section>
);
}
return (
<section>
<Slide key={slide.noteId} slide={slide} />
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
</section>
);
})}
</div>
</div>
);
)
}

View File

@@ -3,8 +3,7 @@
position: relative;
height: 100%;
user-select: none;
display: flex;
flex-direction: column;
padding: 0 5px 0 10px;
.tabulator-tableholder {
height: unset !important;

View File

@@ -1,22 +1,20 @@
import "./index.css";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { DataTreeModule, EditModule, FormatModule, FrozenColumnsModule, InteractionModule, MoveColumnsModule, MoveRowsModule, Options, PersistenceModule, ResizeColumnsModule, RowComponent,SortModule, Tabulator as VanillaTabulator} from 'tabulator-tables';
import { t } from "../../../services/i18n";
import SpacedUpdate from "../../../services/spaced_update";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import CollectionProperties from "../../note_bars/CollectionProperties";
import { ButtonOrActionButton } from "../../react/Button";
import { useLegacyWidget } from "../../react/hooks";
import { ParentComponent } from "../../react/react_utils";
import { ViewModeProps } from "../interface";
import useColTableEditing from "./col_editing";
import { useContextMenu } from "./context_menu";
import useData, { TableConfig } from "./data";
import useRowTableEditing from "./row_editing";
import { TableData } from "./rows";
import { useLegacyWidget } from "../../react/hooks";
import Tabulator from "./tabulator";
import { Tabulator as VanillaTabulator, SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule, Options, RowComponent} from 'tabulator-tables';
import { useContextMenu } from "./context_menu";
import { ParentComponent } from "../../react/react_utils";
import FNote from "../../../entities/fnote";
import { t } from "../../../services/i18n";
import Button from "../../react/Button";
import "./index.css";
import useRowTableEditing from "./row_editing";
import useColTableEditing from "./col_editing";
import AttributeDetailWidget from "../../attribute_widgets/attribute_detail";
import SpacedUpdate from "../../../services/spaced_update";
import useData, { TableConfig } from "./data";
export default function TableView({ note, noteIds, notePath, viewConfig, saveConfig }: ViewModeProps<TableConfig>) {
const tabulatorRef = useRef<VanillaTabulator>(null);
@@ -38,7 +36,7 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
dataTreeChildIndent: 20,
dataTreeExpandElement: `<button class="tree-expand"><span class="bx bx-chevron-right"></span></button>`,
dataTreeCollapseElement: `<button class="tree-collapse"><span class="bx bx-chevron-down"></span></button>`
};
}
}, [ hasChildren ]);
const rowFormatter = useCallback((row: RowComponent) => {
@@ -48,16 +46,6 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
return (
<div className="table-view">
<CollectionProperties
note={note}
rightChildren={note.type !== "search" &&
<>
<ButtonOrActionButton triggerCommand="addNewRow" icon="bx bx-plus" text={t("table_view.new-row")} />
<ButtonOrActionButton triggerCommand="addNewTableColumn" icon="bx bx-carousel" text={t("table_view.new-column")} />
</>
}
/>
{rowData !== undefined && persistenceProps && (
<>
<Tabulator
@@ -66,6 +54,7 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
columns={columnDefs ?? []}
data={rowData}
modules={[ SortModule, FormatModule, InteractionModule, EditModule, ResizeColumnsModule, FrozenColumnsModule, PersistenceModule, MoveColumnsModule, MoveRowsModule, DataTreeModule ]}
footerElement={<TableFooter note={note} />}
events={{
...contextMenuEvents,
...rowEditingEvents
@@ -78,11 +67,24 @@ export default function TableView({ note, noteIds, notePath, viewConfig, saveCon
rowFormatter={rowFormatter}
{...dataTreeProps}
/>
<TableFooter note={note} />
</>
)}
{attributeDetailWidgetEl}
</div>
);
)
}
function TableFooter({ note }: { note: FNote }) {
return (note.type !== "search" &&
<div className="tabulator-footer">
<div className="tabulator-footer-contents">
<Button triggerCommand="addNewRow" icon="bx bx-plus" text={t("table_view.new-row")} />
{" "}
<Button triggerCommand="addNewTableColumn" icon="bx bx-carousel" text={t("table_view.new-column")} />
</div>
</div>
)
}
function usePersistence(viewConfig: TableConfig | null | undefined, saveConfig: (newConfig: TableConfig) => void) {

View File

@@ -5,7 +5,7 @@
> .inline-title,
> .note-detail > .note-detail-editable-text,
> .note-list-widget:not(.full-height) .note-list-wrapper {
> .note-list-widget:not(.full-height) {
padding-inline: 24px;
}

View File

@@ -1,7 +1,7 @@
import appContext from "../../components/app_context";
import { t } from "../../services/i18n";
import options from "../../services/options";
import utils, { isMac } from "../../services/utils";
import utils from "../../services/utils";
/**
* A "call-to-action" is an interactive message for the user, generally to present new features.
@@ -41,6 +41,10 @@ export interface CallToAction {
}[];
}
function isNextTheme() {
return [ "next", "next-light", "next-dark" ].includes(options.get("theme"));
}
const CALL_TO_ACTIONS: CallToAction[] = [
{
id: "new_layout",
@@ -59,7 +63,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
id: "background_effects",
title: t("call_to_action.background_effects_title"),
message: t("call_to_action.background_effects_message"),
enabled: () => (isMac() && !options.is("backgroundEffects")),
enabled: () => false,
buttons: [
{
text: t("call_to_action.background_effects_button"),
@@ -74,7 +78,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
id: "next_theme",
title: t("call_to_action.next_theme_title"),
message: t("call_to_action.next_theme_message"),
enabled: () => ![ "next", "next-light", "next-dark" ].includes(options.get("theme")),
enabled: () => !isNextTheme(),
buttons: [
{
text: t("call_to_action.next_theme_button"),

View File

@@ -3,7 +3,6 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { isDesktop, isMobile } from "../../services/utils";
import TabSwitcher from "../mobile_widgets/TabSwitcher";
import { useTriliumEvent } from "../react/hooks";
import { onWheelHorizontalScroll } from "../widget_utils";
import BookmarkButtons from "./BookmarkButtons";
@@ -98,8 +97,6 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
return <QuickSearchLauncherWidget />;
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />;
case "mobileTabSwitcher":
return <TabSwitcher />;
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}

View File

@@ -1,4 +1,3 @@
import clsx from "clsx";
import { createContext } from "preact";
import { useContext } from "preact/hooks";
@@ -19,12 +18,12 @@ export interface LauncherNoteProps {
launcherNote: FNote;
}
export function LaunchBarActionButton({ className, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition">) {
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
const { isHorizontalLayout } = useContext(LaunchBarContext);
return (
<ActionButton
className={clsx("button-widget launcher-button", className)}
className="button-widget launcher-button"
noIconActionClass
titlePosition={isHorizontalLayout ? "bottom" : "right"}
{...props}

View File

@@ -5,7 +5,6 @@ import { Tooltip } from "bootstrap";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
import type React from "react";
import { Trans } from "react-i18next";
import FNote from "../../entities/fnote";

View File

@@ -26,6 +26,7 @@ export default function NoteTitleActions() {
<div className="title-actions">
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
{!isHiddenNote && note && noteType === "book" && <CollectionProperties note={note} />}
<EditedNotes />
<NoteTypeSwitcher />
</div>

View File

@@ -1,133 +0,0 @@
#launcher-container .mobile-tab-switcher {
position: relative;
&::after {
content: attr(data-tab-count);
font-family: var(--main-font-family);
font-size: 10px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.modal.tab-bar-modal {
.modal-dialog {
min-height: 85vh;
}
.tabs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
@media (min-width: 850px) {
grid-template-columns: 1fr 1fr 1fr;
}
.tab-card {
background: var(--card-background-color);
border-radius: 1em;
min-width: 0;
overflow: hidden;
height: 200px;
display: flex;
flex-direction: column;
&.with-hue {
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
}
&.active {
outline: 4px solid var(--more-accented-background-color);
background: var(--card-background-hover-color);
.title {
font-weight: bold;
}
}
header {
padding: 0.4em 0.5em;
border-bottom: 1px solid rgba(150, 150, 150, 0.1);
display: flex;
overflow: hidden;
align-items: center;
color: var(--custom-color, inherit);
flex-shrink: 0;
&:not(:first-of-type) {
border-top: 1px solid rgba(150, 150, 150, 0.1);
}
>.tn-icon {
margin-inline-end: 0.4em;
}
.title {
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
font-size: 0.9em;
flex-grow: 1;
}
.icon-action {
flex-shrink: 0;
}
}
.tab-preview {
flex-grow: 1;
height: 100%;
overflow: hidden;
font-size: 0.5em;
user-select: none;
pointer-events: none;
&.type-text {
padding: 10px;
}
&.type-book,
&.type-contentWidget,
&.type-search,
&.type-empty {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25em;
color: var(--muted-text-color);
}
.preview-placeholder {
font-size: 500%;
}
p { margin-bottom: 0.2em;}
h2 { font-size: 1.20em; }
h3 { font-size: 1.15em; }
h4 { font-size: 1.10em; }
h5 { font-size: 1.05em}
h6 { font-size: 1em; }
}
&.with-split {
.preview-placeholder {
font-size: 250%;
}
}
}
}
.modal-footer {
.tn-link {
color: var(--main-text-color);
width: 40%;
text-align: center;
text-decoration: none;
}
}
}

View File

@@ -1,240 +0,0 @@
import "./TabSwitcher.css";
import clsx from "clsx";
import { createPortal, Fragment } from "preact/compat";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import contextMenu from "../../menus/context_menu";
import { getHue, parseColor } from "../../services/css_class_manager";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import { NoteContent } from "../collections/legacy/ListOrGridView";
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
import ActionButton from "../react/ActionButton";
import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks";
import Icon from "../react/Icon";
import LinkButton from "../react/LinkButton";
import Modal from "../react/Modal";
export default function TabSwitcher() {
const [ shown, setShown ] = useState(false);
const mainNoteContexts = useMainNoteContexts();
return (
<>
<LaunchBarActionButton
className="mobile-tab-switcher"
icon="bx bx-rectangle"
text="Tabs"
onClick={() => setShown(true)}
data-tab-count={mainNoteContexts.length > 99 ? "∞" : mainNoteContexts.length}
/>
{createPortal(<TabBarModal mainNoteContexts={mainNoteContexts} shown={shown} setShown={setShown} />, document.body)}
</>
);
}
function TabBarModal({ mainNoteContexts, shown, setShown }: {
mainNoteContexts: NoteContext[];
shown: boolean;
setShown: (newValue: boolean) => void;
}) {
const [ fullyShown, setFullyShown ] = useState(false);
const selectTab = useCallback((noteContextToActivate: NoteContext) => {
appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId);
setShown(false);
}, [ setShown ]);
return (
<Modal
className="tab-bar-modal"
size="xl"
title={t("mobile_tab_switcher.title", { count: mainNoteContexts.length})}
show={shown}
onShown={() => setFullyShown(true)}
customTitleBarButtons={[
{
iconClassName: "bx bx-dots-vertical-rounded",
title: t("mobile_tab_switcher.more_options"),
onClick(e) {
contextMenu.show<CommandNames>({
x: e.pageX,
y: e.pageY,
items: [
{ title: t("tab_row.new_tab"), command: "openNewTab", uiIcon: "bx bx-plus" },
{ title: t("tab_row.reopen_last_tab"), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0 },
{ kind: "separator" },
{ title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-trash destructive-action-icon" },
],
selectMenuItemHandler: ({ command }) => {
if (command) {
appContext.triggerCommand(command);
}
}
});
},
}
]}
footer={<>
<LinkButton
text={t("tab_row.new_tab")}
onClick={() => {
appContext.triggerCommand("openNewTab");
setShown(false);
}}
/>
</>}
scrollable
onHidden={() => {
setShown(false);
setFullyShown(false);
}}
>
<TabBarModelContent mainNoteContexts={mainNoteContexts} selectTab={selectTab} shown={fullyShown} />
</Modal>
);
}
function TabBarModelContent({ mainNoteContexts, selectTab, shown }: {
mainNoteContexts: NoteContext[];
shown: boolean;
selectTab: (noteContextToActivate: NoteContext) => void;
}) {
const activeNoteContext = useActiveNoteContext();
const tabRefs = useRef<Record<string, HTMLDivElement | null>>({});
// Scroll to active tab.
useEffect(() => {
if (!shown || !activeNoteContext?.ntxId) return;
const correspondingEl = tabRefs.current[activeNoteContext.ntxId];
requestAnimationFrame(() => {
correspondingEl?.scrollIntoView();
});
}, [ activeNoteContext, shown ]);
return (
<div className="tabs">
{mainNoteContexts.map((noteContext) => (
<Tab
key={noteContext.ntxId}
noteContext={noteContext}
activeNtxId={activeNoteContext.ntxId}
selectTab={selectTab}
containerRef={el => (tabRefs.current[noteContext.ntxId ?? ""] = el)}
/>
))}
</div>
);
}
function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
containerRef: (el: HTMLDivElement | null) => void;
noteContext: NoteContext;
selectTab: (noteContextToActivate: NoteContext) => void;
activeNtxId: string | null | undefined;
}) {
const { note } = noteContext;
const iconClass = useNoteIcon(note);
const colorClass = note?.getColorClass() || '';
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
const subContexts = noteContext.getSubContexts();
return (
<div
ref={containerRef}
class={clsx("tab-card", {
active: noteContext.ntxId === activeNtxId,
"with-hue": workspaceTabBackgroundColorHue !== undefined,
"with-split": subContexts.length > 1
})}
onClick={() => selectTab(noteContext)}
style={{
"--bg-hue": workspaceTabBackgroundColorHue
}}
>
{subContexts.map(subContext => (
<Fragment key={subContext.ntxId}>
<header className={colorClass}>
{subContext.note && <Icon icon={iconClass} />}
<span className="title">{subContext.note?.title ?? t("tab_row.new_tab")}</span>
{subContext.isMainContext() && <ActionButton
icon="bx bx-x"
text={t("tab_row.close_tab")}
onClick={(e) => {
// We are closing a tab, so we need to prevent propagation for click (activate tab).
e.stopPropagation();
appContext.tabManager.removeNoteContext(subContext.ntxId);
}}
/>}
</header>
<div className={clsx("tab-preview", `type-${subContext.note?.type ?? "empty"}`)}>
<TabPreviewContent note={subContext.note} />
</div>
</Fragment>
))}
</div>
);
}
function TabPreviewContent({ note }: {
note: FNote | null
}) {
if (!note) {
return <PreviewPlaceholder icon="bx bx-plus" />;
}
if (note.type === "book") {
return <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
}
return (
<NoteContent
note={note}
highlightedTokens={undefined}
trim
includeArchivedNotes={false}
/>
);
}
function PreviewPlaceholder({ icon}: {
icon: string;
}) {
return (
<div className="preview-placeholder">
<Icon icon={icon} />
</div>
);
}
function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) {
if (!noteContext.hoistedNoteId) return;
const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId);
if (!hoistedNote) return;
const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor();
if (!workspaceTabBackgroundColor) return;
try {
const parsedColor = parseColor(workspaceTabBackgroundColor);
if (!parsedColor) return;
return getHue(parsedColor);
} catch (e) {
// Colors are non-critical, simply ignore.
console.warn(e);
}
}
function useMainNoteContexts() {
const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts());
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ] , () => {
setNoteContexts(appContext.tabManager.getMainNoteContexts());
});
return noteContexts;
}

View File

@@ -1,16 +1,13 @@
import { useContext } from "preact/hooks";
import appContext, { CommandMappings } from "../../components/app_context";
import contextMenu, { MenuItem } from "../../menus/context_menu";
import branches from "../../services/branches";
import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import note_create from "../../services/note_create";
import tree from "../../services/tree";
import { openInAppHelpFromUrl } from "../../services/utils";
import BasicWidget from "../basic_widget";
import ActionButton from "../react/ActionButton";
import { ParentComponent } from "../react/react_utils";
import BasicWidget from "../basic_widget";
export default function MobileDetailMenu() {
const parentComponent = useContext(ParentComponent);
@@ -27,7 +24,6 @@ export default function MobileDetailMenu() {
const subContexts = noteContext.getMainContext().getSubContexts();
const isMainContext = noteContext?.isMainContext();
const note = noteContext.note;
const helpUrl = getHelpUrlForNote(note);
const items: (MenuItem<keyof CommandMappings>)[] = [
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
@@ -35,12 +31,6 @@ export default function MobileDetailMenu() {
{ kind: "separator" },
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
{ kind: "separator" },
helpUrl && {
title: t("help-button.title"),
uiIcon: "bx bx-help-circle",
handler: () => openInAppHelpFromUrl(helpUrl)
},
{ kind: "separator" },
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
@@ -80,5 +70,5 @@ export default function MobileDetailMenu() {
});
}}
/>
);
)
}

View File

@@ -1,5 +1,5 @@
.collection-properties {
padding: 0.55em 12px;
padding: 0;
display: flex;
gap: 0.25em;
align-items: center;
@@ -14,31 +14,7 @@
}
}
>div {
display: inline-flex;
align-items: center;
gap: 0.5em;
}
.center-container {
.spacer {
flex-grow: 1;
justify-content: center;
}
.right-container {
justify-content: flex-end;
}
button.btn {
min-width: 0;
}
@media (max-width: 991px) {
flex-wrap: wrap;
padding: 0.55em 1em;
>div {
flex-grow: 1;
}
}
}

View File

@@ -1,22 +1,24 @@
import "./CollectionProperties.css";
import { t } from "i18next";
import { ComponentChildren } from "preact";
import { useContext, useRef } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import FNote from "../../entities/fnote";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
grid: "bx bxs-grid",
list: "bx bx-list-ul",
calendar: "bx bx-calendar",
@@ -26,26 +28,15 @@ export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
presentation: "bx bx-rectangle"
};
export default function CollectionProperties({ note, centerChildren, rightChildren }: {
note: FNote;
centerChildren?: ComponentChildren;
rightChildren?: ComponentChildren;
}) {
export default function CollectionProperties({ note }: { note: FNote }) {
const [ viewType, setViewType ] = useViewType(note);
const noteType = useNoteProperty(note, "type");
return ([ "book", "search" ].includes(noteType ?? "") &&
return (
<div className="collection-properties">
<div className="left-container">
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
<ViewOptions note={note} viewType={viewType} />
</div>
<div className="center-container">
{centerChildren}
</div>
<div className="right-container">
{rightChildren}
</div>
<ViewTypeSwitcher viewType={viewType} setViewType={setViewType} />
<ViewOptions note={note} viewType={viewType} />
<div className="spacer" />
<HelpButton note={note} />
</div>
);
}
@@ -231,3 +222,15 @@ function CheckBoxPropertyView({ note, property }: { note: FNote, property: Check
/>
);
}
function HelpButton({ note }: { note: FNote }) {
const helpUrl = getHelpUrlForNote(note);
return (helpUrl && (
<ActionButton
icon="bx bx-help-circle"
onClick={(() => openInAppHelpFromUrl(helpUrl))}
text={t("help-button.title")}
/>
));
}

View File

@@ -6,7 +6,6 @@ import clsx from "clsx";
import { t } from "i18next";
import { CSSProperties, RefObject } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type React from "react";
import { CellComponentProps, Grid } from "react-window";
import FNote from "../entities/fnote";
@@ -154,10 +153,10 @@ function NoteIconList({ note, dropdownRef }: {
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
filteredIcons: IconWithName[];
}>) {
}>): React.JSX.Element {
const iconIndex = rowIndex * 12 + columnIndex;
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
if (!iconData) return <></> as React.ReactElement;
if (!iconData) return <></>;
const { id, terms, iconPack } = iconData;
return (
@@ -167,7 +166,7 @@ function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellCompo
title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })}
style={style as CSSProperties}
/>
) as React.ReactElement;
);
}
function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {

View File

@@ -1,11 +1,10 @@
import { HTMLAttributes } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import keyboard_actions from "../../services/keyboard_actions";
import { useStaticTooltip } from "./hooks";
import keyboard_actions from "../../services/keyboard_actions";
import { HTMLAttributes } from "preact";
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu" | "style"> {
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
text: string;
titlePosition?: "top" | "right" | "bottom" | "left";
icon: string;

View File

@@ -1,11 +1,8 @@
import type { ComponentChildren, RefObject } from "preact";
import type { CSSProperties } from "preact/compat";
import { memo } from "preact/compat";
import { useMemo } from "preact/hooks";
import { memo } from "preact/compat";
import { CommandNames } from "../../components/app_context";
import { isDesktop } from "../../services/utils";
import ActionButton from "./ActionButton";
import Icon from "./Icon";
export interface ButtonProps {
@@ -81,7 +78,7 @@ export function ButtonGroup({ children }: { children: ComponentChildren }) {
<div className="btn-group" role="group">
{children}
</div>
);
)
}
export function SplitButton({ text, icon, children, ...restProps }: {
@@ -106,17 +103,7 @@ export function SplitButton({ text, icon, children, ...restProps }: {
{children}
</ul>
</ButtonGroup>
);
}
export function ButtonOrActionButton(props: {
text: string;
icon: string;
} & Pick<ButtonProps, "onClick" | "triggerCommand" | "disabled" | "title">) {
if (isDesktop()) {
return <Button {...props} />;
}
return <ActionButton {...props} />;
)
}
export default Button;

View File

@@ -1,7 +1,7 @@
import clsx from "clsx";
import { HTMLAttributes } from "preact";
interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" | "onClick" | "title"> {
interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" | "onClick"> {
icon?: string;
className?: string;
}

View File

@@ -1,17 +1,17 @@
import { Modal as BootstrapModal } from "bootstrap";
import clsx from "clsx";
import { ComponentChildren, CSSProperties, RefObject } from "preact";
import { memo } from "preact/compat";
import { useEffect, useMemo, useRef } from "preact/hooks";
import { openDialog } from "../../services/dialog";
import { useEffect, useRef, useMemo } from "preact/hooks";
import { t } from "../../services/i18n";
import { ComponentChildren } from "preact";
import type { CSSProperties, RefObject } from "preact/compat";
import { openDialog } from "../../services/dialog";
import { Modal as BootstrapModal } from "bootstrap";
import { memo } from "preact/compat";
import { useSyncedRef } from "./hooks";
interface CustomTitleBarButton {
title: string;
iconClassName: string;
onClick: (e: MouseEvent) => void;
onClick: () => void;
}
export interface ModalProps {
@@ -80,7 +80,7 @@ export interface ModalProps {
noFocus?: boolean;
}
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
const modalInstanceRef = useRef<BootstrapModal>();
const elementToFocus = useRef<Element | null>();
@@ -116,7 +116,7 @@ export default function Modal({ children, className, size, title, customTitleBar
focus: !noFocus
}).then(($widget) => {
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
});
})
} else {
modalInstanceRef.current?.hide();
}
@@ -159,12 +159,13 @@ export default function Modal({ children, className, size, title, customTitleBar
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
<button type="button"
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
title={titleBarButton.title}
onClick={titleBarButton.onClick} />
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
title={titleBarButton.title}
onClick={titleBarButton.onClick}>
</button>
))}
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")} />
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
</div>

View File

@@ -182,10 +182,10 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: N
/>;
}
export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCustomInnerProps) {
function ToggleReadOnlyButton({ note, viewType, 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" ].includes(note.type) || viewType === "geoMap" || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <ActionButton
@@ -234,9 +234,9 @@ function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
/>;
}
function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
function InAppHelpButton({ note, noteType }: NoteActionsCustomInnerProps) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl;
const isEnabled = !!helpUrl && (noteType !== "book");
return isEnabled && (
<ActionButton
@@ -247,8 +247,15 @@ function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
);
}
function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
if (noteType === "relationMap") {
function AddChildButton({ parentComponent, noteType, viewType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
if (noteType === "book" && viewType === "geoMap") {
return <ActionButton
icon="bx bx-plus-circle"
text={t("geo-map.create-child-note-title")}
onClick={() => parentComponent.triggerEvent("geoMapCreateChildNote", { ntxId })}
disabled={isReadOnly}
/>;
} else if (noteType === "relationMap") {
return <ActionButton
icon="bx bx-folder-plus"
text={t("relation_map_buttons.create_child_note_title")}

View File

@@ -7,6 +7,7 @@ import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import bulk_action, { ACTION_GROUPS } from "../../services/bulk_action";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import server from "../../services/server";
@@ -15,6 +16,7 @@ import tree from "../../services/tree";
import { getErrorMessage } from "../../services/utils";
import ws from "../../services/ws";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import CollectionProperties from "../note_bars/CollectionProperties";
import Button from "../react/Button";
import Dropdown from "../react/Dropdown";
import { FormListHeader, FormListItem } from "../react/FormList";
@@ -24,6 +26,8 @@ import { ParentComponent } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabContext, "note" | "ntxId" | "hidden">) {
const parentComponent = useContext(ParentComponent);
const [ searchOptions, setSearchOptions ] = useState<{ availableOptions: SearchOption[], activeOptions: SearchOption[] }>();
@@ -111,6 +115,11 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
defaultValue={defaultValue}
/>;
})}
{isNewLayout && <tr className="view-options">
<td className="title-column">{t("search_definition.view_options")}</td>
<td><CollectionProperties note={note} /></td>
</tr>}
</tbody>
<BulkActionsList note={note} />
<tbody className="search-actions">

View File

@@ -459,7 +459,7 @@ body.experimental-feature-new-layout {
gap: var(--button-gap);
&> button:last-of-type {
margin-right: 0.5em;
margin-right: 1em;
}
}
}

View File

@@ -18,7 +18,6 @@ import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
import Icon from "../../react/Icon";
import OptionsSection from "./components/OptionsSection";
import PlatformIndicator from "./components/PlatformIndicator";
import RadioWithIllustration from "./components/RadioWithIllustration";
import RelatedSettings from "./components/RelatedSettings";
@@ -175,13 +174,13 @@ function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
</div>
</div>
) : (
<div>
<div className="title-bar">
<Icon icon="bx bx-leaf" />
<span className="title">Title</span>
<Icon icon="bx bx-dock-right" />
</div>
<div>
<div className="title-bar">
<Icon icon="bx bx-leaf" />
<span className="title">Title</span>
<Icon icon="bx bx-dock-right" />
</div>
</div>
)}
{!isNewLayout && <div className="ribbon">
@@ -193,7 +192,7 @@ function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
</div>
<div className="ribbon-body">
<div className="ribbon-body-content" />
<div className="ribbon-body-content"></div>
</div>
</div>}
@@ -357,11 +356,7 @@ function ElectronIntegration() {
<FormGroup name="background-effects" description={t("electron_integration.background-effects-description")}>
<FormCheckbox
label={<>
{t("electron_integration.background-effects")}
{" "}
<PlatformIndicator windows="11" mac />
</>}
label={t("electron_integration.background-effects")}
currentValue={backgroundEffects} onChange={setBackgroundEffects}
disabled={nativeTitleBarVisible}
/>

View File

@@ -1,5 +0,0 @@
.platform-indicator {
display: inline-flex;
gap: 0.25em;
color: var(--muted-text-color);
}

View File

@@ -1,34 +0,0 @@
import "./PlatformIndicator.css";
import { useRef } from "preact/hooks";
import { t } from "../../../../services/i18n";
import { useStaticTooltip } from "../../../react/hooks";
import Icon from "../../../react/Icon";
interface PlatformIndicatorProps {
windows?: boolean | "11";
mac: boolean;
}
export default function PlatformIndicator({ windows, mac }: PlatformIndicatorProps) {
const containerRef = useRef<HTMLDivElement>(null);
useStaticTooltip(containerRef, {
selector: "span",
animation: false,
title() { return this.title; },
});
return (
<div ref={containerRef} className="platform-indicator">
{windows && <Icon
icon="bx bxl-windows"
title={t("platform_indicator.available_on", { platform: windows === "11" ? "Windows 11" : "Windows" })}
/>}
{mac && <Icon
icon="bx bxl-apple"
title={t("platform_indicator.available_on", { platform: "macOS" })}
/>}
</div>
);
}

View File

@@ -1,22 +1,20 @@
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
import { useMemo } from "preact/hooks";
import type React from "react";
import { Trans } from "react-i18next";
import { t } from "../../../services/i18n";
import search from "../../../services/search";
import server from "../../../services/server";
import toast from "../../../services/toast";
import { isElectron } from "../../../services/utils";
import Button from "../../react/Button";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import FormSelect from "../../react/FormSelect";
import FormText from "../../react/FormText";
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
import OptionsSection from "./components/OptionsSection";
import TimeSelector from "./components/TimeSelector";
import { useMemo } from "preact/hooks";
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
import FormCheckbox from "../../react/FormCheckbox";
import FormGroup from "../../react/FormGroup";
import search from "../../../services/search";
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
import FormSelect from "../../react/FormSelect";
import { isElectron } from "../../../services/utils";
export default function OtherSettings() {
return (
@@ -33,7 +31,7 @@ export default function OtherSettings() {
<ShareSettings />
<NetworkSettings />
</>
);
)
}
function SearchEngineSettings() {
@@ -84,7 +82,7 @@ function SearchEngineSettings() {
/>
</FormGroup>
</OptionsSection>
);
)
}
function TrayOptionsSettings() {
@@ -99,7 +97,7 @@ function TrayOptionsSettings() {
onChange={trayEnabled => setDisableTray(!trayEnabled)}
/>
</OptionsSection>
);
)
}
function NoteErasureTimeout() {
@@ -107,13 +105,13 @@ function NoteErasureTimeout() {
<OptionsSection title={t("note_erasure_timeout.note_erasure_timeout_title")}>
<FormText>{t("note_erasure_timeout.note_erasure_description")}</FormText>
<FormGroup name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")}>
<TimeSelector
name="erase-entities-after"
<TimeSelector
name="erase-entities-after"
optionValueId="eraseEntitiesAfterTimeInSeconds" optionTimeScaleId="eraseEntitiesAfterTimeScale"
/>
</FormGroup>
<FormText>{t("note_erasure_timeout.manual_erasing_description")}</FormText>
<Button
text={t("note_erasure_timeout.erase_deleted_notes_now")}
onClick={() => {
@@ -123,7 +121,7 @@ function NoteErasureTimeout() {
}}
/>
</OptionsSection>
);
)
}
function AttachmentErasureTimeout() {
@@ -147,7 +145,7 @@ function AttachmentErasureTimeout() {
}}
/>
</OptionsSection>
);
)
}
function RevisionSnapshotInterval() {
@@ -167,7 +165,7 @@ function RevisionSnapshotInterval() {
/>
</FormGroup>
</OptionsSection>
);
)
}
function RevisionSnapshotLimit() {
@@ -178,7 +176,7 @@ function RevisionSnapshotLimit() {
<FormText>{t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}</FormText>
<FormGroup name="revision-snapshot-number-limit">
<FormTextBoxWithUnit
<FormTextBoxWithUnit
type="number" min={-1}
currentValue={revisionSnapshotNumberLimit}
unit={t("revisions_snapshot_limit.snapshot_number_limit_unit")}
@@ -199,7 +197,7 @@ function RevisionSnapshotLimit() {
}}
/>
</OptionsSection>
);
)
}
function HtmlImportTags() {
@@ -238,7 +236,7 @@ function HtmlImportTags() {
onClick={() => setAllowedHtmlTags(SANITIZER_DEFAULT_ALLOWED_TAGS)}
/>
</OptionsSection>
);
)
}
function ShareSettings() {
@@ -248,8 +246,8 @@ function ShareSettings() {
return (
<OptionsSection title={t("share.title")}>
<FormGroup name="redirectBareDomain" description={t("share.redirect_bare_domain_description")}>
<FormCheckbox
label={t(t("share.redirect_bare_domain"))}
<FormCheckbox
label={t(t("share.redirect_bare_domain"))}
currentValue={redirectBareDomain}
onChange={async value => {
if (value) {
@@ -266,17 +264,17 @@ function ShareSettings() {
}
setRedirectBareDomain(value);
}}
/>
/>
</FormGroup>
<FormGroup name="showLoginInShareTheme" description={t("share.show_login_link_description")}>
<FormCheckbox
<FormCheckbox
label={t("share.show_login_link")}
currentValue={showLogInShareTheme} onChange={setShowLogInShareTheme}
/>
</FormGroup>
</OptionsSection>
);
)
}
function NetworkSettings() {
@@ -290,5 +288,5 @@ function NetworkSettings() {
currentValue={checkForUpdates} onChange={setCheckForUpdates}
/>
</OptionsSection>
);
}
)
}

View File

@@ -2,7 +2,6 @@ import { normalizeMimeTypeForCKEditor, type OptionNames } from "@triliumnext/com
import { Themes } from "@triliumnext/highlightjs";
import type { CSSProperties } from "preact/compat";
import { useEffect, useMemo, useState } from "preact/hooks";
import type React from "react";
import { Trans } from "react-i18next";
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";

View File

@@ -85,7 +85,7 @@ export default defineConfig(() => ({
sourcemap: false,
rollupOptions: {
input: {
index: join(__dirname, "index.html"),
index: join(__dirname, "src", "index.html"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
set_password: join(__dirname, "src", "set_password.ts"),

View File

@@ -1 +0,0 @@
electron-forge/app-icon/mac

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,11 +1,9 @@
import type { ForgeConfig } from "@electron-forge/shared-types";
import { LOCALES } from "@triliumnext/commons";
import { existsSync } from "fs";
import fs from "fs-extra";
import path, { join } from "path";
import packageJson from "../package.json" assert { type: "json" };
import fs from "fs-extra";
import { LOCALES } from "@triliumnext/commons";
import { PRODUCT_NAME } from "../src/app-info.js";
import type { ForgeConfig } from "@electron-forge/shared-types";
import { existsSync } from "fs";
const ELECTRON_FORGE_DIR = __dirname;
@@ -14,12 +12,12 @@ const APP_ICON_PATH = path.join(ELECTRON_FORGE_DIR, "app-icon");
const extraResourcesForPlatform = getExtraResourcesForPlatform();
const baseLinuxMakerConfigOptions = {
name: EXECUTABLE_NAME,
bin: EXECUTABLE_NAME,
productName: PRODUCT_NAME,
icon: path.join(APP_ICON_PATH, "png/128x128.png"),
desktopTemplate: path.resolve(path.join(ELECTRON_FORGE_DIR, "desktop.ejs")),
categories: ["Office", "Utility"]
name: EXECUTABLE_NAME,
bin: EXECUTABLE_NAME,
productName: PRODUCT_NAME,
icon: path.join(APP_ICON_PATH, "png/128x128.png"),
desktopTemplate: path.resolve(path.join(ELECTRON_FORGE_DIR, "desktop.ejs")),
categories: ["Office", "Utility"]
};
const windowsSignConfiguration = process.env.WINDOWS_SIGN_EXECUTABLE ? {
hookModulePath: path.join(ELECTRON_FORGE_DIR, "sign-windows.cjs")
@@ -32,7 +30,6 @@ const macosSignConfiguration = process.env.APPLE_ID ? {
teamId: process.env.APPLE_TEAM_ID!
}
} : undefined;
const isNightly = packageJson.version.includes("test");
const config: ForgeConfig = {
outDir: "out",
@@ -40,10 +37,9 @@ const config: ForgeConfig = {
packagerConfig: {
executableName: EXECUTABLE_NAME,
name: PRODUCT_NAME,
appVersion: packageJson.version,
overwrite: true,
asar: true,
icon: path.join(APP_ICON_PATH, isNightly ? "icon-dev" : "icon"),
icon: path.join(APP_ICON_PATH, "icon"),
...macosSignConfiguration,
windowsSign: windowsSignConfiguration,
extraResource: [
@@ -91,7 +87,7 @@ const config: ForgeConfig = {
...baseLinuxMakerConfigOptions,
desktopTemplate: undefined, // otherwise it would put in the wrong exec
icon: {
"128x128": path.join(APP_ICON_PATH, isNightly ? "png/128x128-dev.png" : "png/128x128.png"),
"128x128": path.join(APP_ICON_PATH, "png/128x128.png"),
},
id: "com.triliumnext.notes",
runtimeVersion: "24.08",
@@ -140,24 +136,24 @@ const config: ForgeConfig = {
config: {
name: EXECUTABLE_NAME,
productName: PRODUCT_NAME,
iconUrl: `https://raw.githubusercontent.com/TriliumNext/Trilium/refs/heads/main/apps/desktop/electron-forge/app-icon/${isNightly ? "icon-dev" : "icon"}.ico`,
setupIcon: path.join(ELECTRON_FORGE_DIR, isNightly ? "setup-icon/setup-dev.ico" : "setup-icon/setup.ico"),
loadingGif: path.join(ELECTRON_FORGE_DIR, isNightly ? "setup-icon/setup-banner-dev.gif" : "setup-icon/setup-banner.gif"),
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Trilium/refs/heads/main/apps/desktop/electron-forge/app-icon/icon.ico",
setupIcon: path.join(ELECTRON_FORGE_DIR, "setup-icon/setup.ico"),
loadingGif: path.join(ELECTRON_FORGE_DIR, "setup-icon/setup-banner.gif"),
windowsSign: windowsSignConfiguration
}
},
{
name: "@electron-forge/maker-dmg",
config: {
icon: path.join(APP_ICON_PATH, isNightly ? "icon-dev.icns" : "icon.icns")
icon: path.join(APP_ICON_PATH, "icon.icns")
}
},
{
name: "@electron-forge/maker-zip",
config: {
options: {
iconUrl: `https://raw.githubusercontent.com/TriliumNext/Trilium/refs/heads/main/apps/desktop/electron-forge/app-icon/${isNightly ? "icon-dev" : "icon"}.ico`,
icon: path.join(APP_ICON_PATH, isNightly ? "icon-dev.ico" : "icon.ico")
iconUrl: "https://raw.githubusercontent.com/TriliumNext/Trilium/refs/heads/main/apps/desktop/electron-forge/app-icon/icon.ico",
icon: path.join(APP_ICON_PATH, "icon.ico")
}
}
}
@@ -176,7 +172,7 @@ const config: ForgeConfig = {
.filter(locale => !locale.contentOnly)
.map(locale => locale.electronLocale) as string[];
if (!isMac) {
localesToKeep = localesToKeep.map(locale => locale.replace("_", "-"));
localesToKeep = localesToKeep.map(locale => locale.replace("_", "-"))
}
const keptLocales = new Set();
@@ -287,11 +283,11 @@ function getExtraResourcesForPlatform() {
const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"];
const scriptExt = (process.platform === "win32") ? "bat" : "sh";
return scripts.map(script => `electron-forge/${script}.${scriptExt}`);
};
}
switch (process.platform) {
case "win32":
resources.push(...getScriptResources());
resources.push(...getScriptResources())
break;
case "linux":
resources.push(...getScriptResources(), path.join(APP_ICON_PATH, "png/256x256.png"));
@@ -304,18 +300,18 @@ function getExtraResourcesForPlatform() {
}
function getELFArch(file: string) {
const buf = fs.readFileSync(file);
const buf = fs.readFileSync(file);
if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46) {
throw new Error("Not an ELF file");
}
if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46) {
throw new Error("Not an ELF file");
}
const eiClass = buf[4]; // 1=32-bit, 2=64-bit
const eiMachine = buf[18]; // architecture code
const eiClass = buf[4]; // 1=32-bit, 2=64-bit
const eiMachine = buf[18]; // architecture code
if (eiMachine === 0x3E) return 'x86-64';
if (eiMachine === 0xB7) return 'ARM64';
return 'other';
if (eiMachine === 0x3E) return 'x86-64';
if (eiMachine === 0xB7) return 'ARM64';
return 'other';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.1.0",
"electron": "40.0.0",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "40.1.0",
"electron": "40.0.0",
"fs-extra": "11.3.3"
},
"scripts": {

View File

@@ -17,17 +17,17 @@ test("Can drag tabs around", async ({ page, context }) => {
await app.addNewTab();
await app.addNewTab();
let tab = await app.getTab(0);
let tab = app.getTab(0);
// Drag the first tab at the end
await tab.dragTo(await app.getTab(2), { targetPosition: { x: 50, y: 0 } });
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 } });
tab = await app.getTab(2);
tab = app.getTab(2);
await expect(tab).toContainText(NOTE_TITLE);
// Drag the tab to the left
await tab.dragTo(await app.getTab(0), { targetPosition: { x: 50, y: 0 } });
await expect(await app.getTab(0)).toContainText(NOTE_TITLE);
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 } });
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
});
test("Can drag tab to new window", async ({ page, context }) => {
@@ -36,7 +36,7 @@ test("Can drag tab to new window", async ({ page, context }) => {
await app.closeAllTabs();
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
const tab = await app.getTab(0);
const tab = app.getTab(0);
await expect(tab).toContainText(NOTE_TITLE);
const popupPromise = page.waitForEvent("popup");
@@ -75,14 +75,14 @@ test("Tabs are restored in right order", async ({ page, context }) => {
await expect(app.getActiveTab()).toContainText("Mermaid");
// Select the mid one.
await (await app.getTab(1)).click();
await app.getTab(1).click();
await expect(app.noteTreeActiveNote).toContainText("Text notes");
// Refresh the page and check the order.
await app.goto( { preserveTabs: true });
await expect(await app.getTab(0)).toContainText("Code notes");
await expect(await app.getTab(1)).toContainText("Text notes");
await expect(await app.getTab(2)).toContainText("Mermaid");
await expect(app.getTab(0)).toContainText("Code notes");
await expect(app.getTab(1)).toContainText("Text notes");
await expect(app.getTab(2)).toContainText("Mermaid");
// Check the note tree has the right active node.
await expect(app.noteTreeActiveNote).toContainText("Text notes");
@@ -118,7 +118,7 @@ test("Search works when dismissing a tab", async ({ page, context }) => {
await app.addNewTab();
await app.goToNoteInNewTab("Sample mindmap");
await (await app.getTab(0)).click();
await app.getTab(0).click();
await app.openAndClickNoteActionMenu("Search in note");
await expect(app.findAndReplaceWidget.first()).toBeVisible();
});

View File

@@ -26,7 +26,6 @@ export default class App {
readonly currentNoteSplitTitle: Locator;
readonly currentNoteSplitContent: Locator;
readonly sidebar: Locator;
private isMobile: boolean = false;
constructor(page: Page, context: BrowserContext) {
this.page = page;
@@ -44,8 +43,6 @@ export default class App {
}
async goto({ url, isMobile, preserveTabs }: GotoOpts = {}) {
this.isMobile = !!isMobile;
await this.context.addCookies([
{
url: BASE_URL,
@@ -62,7 +59,7 @@ export default class App {
// Wait for the page to load.
if (url === "/") {
await expect(this.page.locator(".tree", { hasText: "Trilium Integration Test" })).toBeVisible();
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
if (!preserveTabs) {
await this.closeAllTabs();
}
@@ -79,19 +76,14 @@ export default class App {
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
await expect(suggestionSelector).toContainText(noteTitle);
await suggestionSelector.click();
suggestionSelector.click();
}
async goToSettings() {
await this.page.locator(".launcher-button.bx-cog").click();
}
async getTab(tabIndex: number) {
if (this.isMobile) {
await this.launcherBar.locator(".mobile-tab-switcher").click();
return this.page.locator(".modal.tab-bar-modal .tab-card").nth(tabIndex);
}
getTab(tabIndex: number) {
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
}
@@ -105,8 +97,7 @@ export default class App {
async closeAllTabs() {
await this.triggerCommand("closeAllTabs");
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
const tab = await this.getTab(0);
await tab.click();
await this.getTab(0).click();
}
/**

View File

@@ -337,155 +337,6 @@ paths:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/revisions:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns all revisions for a note identified by its ID
operationId: getNoteRevisions
responses:
"200":
description: list of revisions
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/Revision"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/attachments:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns all attachments for a note identified by its ID
operationId: getNoteAttachments
responses:
"200":
description: list of attachments
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/Attachment"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/undelete:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
post:
description: Restore a deleted note. The note must be deleted and must have at least one undeleted parent.
operationId: undeleteNote
responses:
"200":
description: note restored successfully
content:
application/json; charset=utf-8:
schema:
type: object
properties:
success:
type: boolean
example: true
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/history:
get:
description: Returns recent changes including note creations, modifications, and deletions
operationId: getNoteHistory
parameters:
- name: ancestorNoteId
in: query
required: false
description: Limit changes to a subtree identified by this note ID. Defaults to "root" (all notes).
schema:
$ref: "#/components/schemas/EntityId"
responses:
"200":
description: list of recent changes
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/RecentChange"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/revisions/{revisionId}:
parameters:
- name: revisionId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns a revision identified by its ID
operationId: getRevisionById
responses:
"200":
description: revision response
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Revision"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/revisions/{revisionId}/content:
parameters:
- name: revisionId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns revision content identified by its ID
operationId: getRevisionContent
responses:
"200":
description: revision content response
content:
text/html:
schema:
type: string
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/branches:
post:
description: >
@@ -1335,93 +1186,3 @@ components:
type: string
description: Human readable error, potentially with more details,
example: Note 'evnnmvHTCgIn' is protected and cannot be modified through ETAPI
Revision:
type: object
description: Revision represents a snapshot of note's title and content at some point in the past.
properties:
revisionId:
$ref: "#/components/schemas/EntityId"
readOnly: true
noteId:
$ref: "#/components/schemas/EntityId"
readOnly: true
type:
type: string
enum:
[
text,
code,
render,
file,
image,
search,
relationMap,
book,
noteMap,
mermaid,
webView,
shortcut,
doc,
contentWidget,
launcher,
]
mime:
type: string
isProtected:
type: boolean
readOnly: true
title:
type: string
blobId:
type: string
description: ID of the blob object which effectively serves as a content hash
dateLastEdited:
$ref: "#/components/schemas/LocalDateTime"
readOnly: true
dateCreated:
$ref: "#/components/schemas/LocalDateTime"
readOnly: true
utcDateLastEdited:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
utcDateCreated:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
utcDateModified:
$ref: "#/components/schemas/UtcDateTime"
readOnly: true
contentLength:
type: integer
format: int32
readOnly: true
RecentChange:
type: object
description: Represents a recent change event (creation, modification, or deletion).
properties:
noteId:
$ref: "#/components/schemas/EntityId"
readOnly: true
title:
type: string
description: Title at the time of the change (may be "[protected]" for protected notes)
current_title:
type: string
description: Current title of the note (may be "[protected]" for protected notes)
current_isDeleted:
type: boolean
description: Whether the note is currently deleted
current_deleteId:
type: string
description: Delete ID if the note is deleted
current_isProtected:
type: boolean
description: Whether the note is protected
utcDate:
$ref: "#/components/schemas/UtcDateTime"
description: UTC timestamp of the change
date:
$ref: "#/components/schemas/LocalDateTime"
description: Local timestamp of the change
canBeUndeleted:
type: boolean
description: Whether the note can be undeleted (only present for deleted notes)

View File

@@ -35,8 +35,8 @@
"sucrase": "3.35.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "0.72.1",
"@braintree/sanitize-url": "7.1.2",
"@anthropic-ai/sdk": "0.71.2",
"@braintree/sanitize-url": "7.1.1",
"@electron/remote": "2.1.3",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
@@ -70,7 +70,7 @@
"@types/xml2js": "0.4.14",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"axios": "1.13.4",
"axios": "1.13.3",
"bindings": "1.5.0",
"bootstrap": "5.3.8",
"chardet": "2.1.1",
@@ -83,7 +83,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "4.0.1",
"electron": "40.1.0",
"electron": "40.0.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -112,7 +112,7 @@
"multer": "2.0.2",
"normalize-strings": "1.1.1",
"ollama": "0.6.3",
"openai": "6.17.0",
"openai": "6.16.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",

View File

@@ -1,77 +0,0 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
describe("etapi/get-note-revisions", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token);
// Create a revision by updating the note content
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Updated content for revision")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
});
it("gets revisions for a note", async () => {
const response = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
const revision = response.body[0];
expect(revision).toHaveProperty("revisionId");
expect(revision).toHaveProperty("noteId", createdNoteId);
expect(revision).toHaveProperty("type");
expect(revision).toHaveProperty("mime");
expect(revision).toHaveProperty("title");
expect(revision).toHaveProperty("isProtected");
expect(revision).toHaveProperty("blobId");
expect(revision).toHaveProperty("utcDateCreated");
});
it("returns empty array for note with no revisions", async () => {
// Create a new note without any revisions
const newNoteId = await createNote(app, token, "Brand new content");
const response = await supertest(app)
.get(`/etapi/notes/${newNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// New notes may or may not have revisions depending on settings
});
it("returns 404 for non-existent note", async () => {
const response = await supertest(app)
.get("/etapi/notes/nonexistentnote/revisions")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
});
});

View File

@@ -1,71 +0,0 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
let revisionId: string;
describe("etapi/get-revision", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token, "Initial content");
// Update content to create a revision
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Updated content")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Get the revision ID
const revisionsResponse = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(revisionsResponse.body.length).toBeGreaterThan(0);
revisionId = revisionsResponse.body[0].revisionId;
});
it("gets revision metadata by ID", async () => {
const response = await supertest(app)
.get(`/etapi/revisions/${revisionId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.body).toHaveProperty("revisionId", revisionId);
expect(response.body).toHaveProperty("noteId", createdNoteId);
expect(response.body).toHaveProperty("type", "text");
expect(response.body).toHaveProperty("mime", "text/html");
expect(response.body).toHaveProperty("title", "Hello");
expect(response.body).toHaveProperty("isProtected", false);
expect(response.body).toHaveProperty("blobId");
expect(response.body).toHaveProperty("utcDateCreated");
expect(response.body).toHaveProperty("utcDateModified");
});
it("returns 404 for non-existent revision", async () => {
const response = await supertest(app)
.get("/etapi/revisions/nonexistentrevision")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
});
});

View File

@@ -1,94 +0,0 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
describe("etapi/note-history", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
// Create a note to ensure there's some history
createdNoteId = await createNote(app, token, "History test content");
// Create a revision to ensure history has entries
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
});
it("gets recent changes history", async () => {
const response = await supertest(app)
.get("/etapi/notes/history")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
// Check that history entries have expected properties
const entry = response.body[0];
expect(entry).toHaveProperty("noteId");
expect(entry).toHaveProperty("title");
expect(entry).toHaveProperty("utcDate");
expect(entry).toHaveProperty("date");
expect(entry).toHaveProperty("current_isDeleted");
expect(entry).toHaveProperty("current_isProtected");
});
it("filters history by ancestor note", async () => {
const response = await supertest(app)
.get("/etapi/notes/history?ancestorNoteId=root")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// All results should be descendants of root (which is everything)
});
it("returns empty array for non-existent ancestor", async () => {
const response = await supertest(app)
.get("/etapi/notes/history?ancestorNoteId=nonexistentancestor")
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
// Should be empty since no notes are descendants of a non-existent note
expect(response.body.length).toBe(0);
});
it("includes canBeUndeleted for deleted notes", async () => {
// Create and delete a note
const noteToDeleteId = await createNote(app, token, "Note to delete for history test");
await supertest(app)
.delete(`/etapi/notes/${noteToDeleteId}`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Check history - deleted note should appear with canBeUndeleted property
const response = await supertest(app)
.get("/etapi/notes/history")
.auth(USER, token, { "type": "basic" })
.expect(200);
const deletedEntry = response.body.find(
(entry: any) => entry.noteId === noteToDeleteId && entry.current_isDeleted === true
);
// Deleted entries should have canBeUndeleted property
if (deletedEntry) {
expect(deletedEntry).toHaveProperty("canBeUndeleted");
}
});
});

View File

@@ -1,64 +0,0 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
let revisionId: string;
describe("etapi/revision-content", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token, "Initial revision content");
// Update content to ensure we have content in the revision
await supertest(app)
.put(`/etapi/notes/${createdNoteId}/content`)
.auth(USER, token, { "type": "basic" })
.set("Content-Type", "text/plain")
.send("Content after first update")
.expect(204);
// Force create a revision
await supertest(app)
.post(`/etapi/notes/${createdNoteId}/revision`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Get the revision ID
const revisionsResponse = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/revisions`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(revisionsResponse.body.length).toBeGreaterThan(0);
revisionId = revisionsResponse.body[0].revisionId;
});
it("gets revision content", async () => {
const response = await supertest(app)
.get(`/etapi/revisions/${revisionId}/content`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.headers["content-type"]).toMatch(/text\/html/);
expect(response.text).toBeTruthy();
});
it("returns 404 for non-existent revision content", async () => {
const response = await supertest(app)
.get("/etapi/revisions/nonexistentrevision/content")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("REVISION_NOT_FOUND");
});
});

View File

@@ -1,103 +0,0 @@
import { Application } from "express";
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
import supertest from "supertest";
import { login } from "./utils.js";
import config from "../../src/services/config.js";
import { randomInt } from "crypto";
let app: Application;
let token: string;
const USER = "etapi";
describe("etapi/undelete-note", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
});
it("undeletes a deleted note", async () => {
// Create a note
const noteId = `testNote${randomInt(10000)}`;
await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic" })
.send({
"noteId": noteId,
"parentNoteId": "root",
"title": "Note to delete and restore",
"type": "text",
"content": "Content to restore"
})
.expect(201);
// Verify note exists
await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
// Delete the note
await supertest(app)
.delete(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(204);
// Verify note is deleted (should return 404)
await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(404);
// Undelete the note
const response = await supertest(app)
.post(`/etapi/notes/${noteId}/undelete`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(response.body).toHaveProperty("success", true);
// Verify note is restored
const restoredResponse = await supertest(app)
.get(`/etapi/notes/${noteId}`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(restoredResponse.body.title).toStrictEqual("Note to delete and restore");
});
it("returns 404 for non-existent note", async () => {
const response = await supertest(app)
.post("/etapi/notes/nonexistentnote/undelete")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
});
it("returns 400 when trying to undelete a non-deleted note", async () => {
// Create a note
const noteId = `testNote${randomInt(10000)}`;
await supertest(app)
.post("/etapi/create-note")
.auth(USER, token, { "type": "basic" })
.send({
"noteId": noteId,
"parentNoteId": "root",
"title": "Note not deleted",
"type": "text",
"content": "Content"
})
.expect(201);
// Try to undelete a note that isn't deleted
const response = await supertest(app)
.post(`/etapi/notes/${noteId}/undelete`)
.auth(USER, token, { "type": "basic" })
.expect(400);
expect(response.body.code).toStrictEqual("NOTE_NOT_DELETED");
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,309 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 256 256"
style="enable-background:new 0 0 256 256;"
xml:space="preserve"
sodipodi:docname="icon-installer-purple.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs34" /><sodipodi:namedview
id="namedview34"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.5449219"
inkscape:cx="96.61708"
inkscape:cy="167.70468"
inkscape:window-width="1536"
inkscape:window-height="1494"
inkscape:window-x="5312"
inkscape:window-y="379"
inkscape:window-maximized="0"
inkscape:current-layer="g20" />
<style
type="text/css"
id="style1">
.st0{fill:#686768;}
.st1{fill:#808080;}
.st2{fill:url(#SVGID_1_);}
.st3{fill:url(#SVGID_2_);}
.st4{fill:url(#SVGID_3_);}
.st5{fill:#D9D9D9;}
.st6{fill:url(#SVGID_4_);}
.st7{opacity:0.47;}
.st8{fill:#5B5A5A;}
.st9{fill:#95C980;}
.st10{fill:#72B755;}
.st11{fill:#4FA52B;}
.st12{fill:#EE8C89;}
.st13{fill:#E96562;}
.st14{fill:#E33F3B;}
.st15{fill:#EFB075;}
.st16{fill:#E99547;}
.st17{fill:#E47B19;}
.st18{opacity:0.38;fill:url(#SVGID_5_);enable-background:new ;}
</style>
<g
id="Layer_1_2_">
<g
id="Layer_1_1_">
</g>
</g>
<g
id="Layer_2_1_">
<polygon
class="st0"
points="69.5,48.6 69.3,93.1 4,95.2 3.3,93.7 29.6,53.4 "
id="polygon1" />
<path
class="st1"
d="M69.5,47l-0.2,46.1c0,0-66.3,1-66,0.6l26.1-41.8L69.5,47z"
id="path1" />
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="69.458"
y1="120.0202"
x2="219.2576"
y2="120.0202"
gradientTransform="matrix(1 0 0 1 0 8)">
<stop
offset="0"
style="stop-color:#E3E3E3"
id="stop1" />
<stop
offset="1"
style="stop-color:#F4F4F4"
id="stop2" />
</linearGradient>
<polygon
class="st2"
points="69.5,47 218.9,55.6 219.3,202.6 69.9,209.1 "
id="polygon2" />
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="29.2408"
y1="120.0202"
x2="69.8681"
y2="120.0202"
gradientTransform="matrix(1 0 0 1 0 8)">
<stop
offset="0"
style="stop-color:#D9D9D9"
id="stop3" />
<stop
offset="1"
style="stop-color:#D4D4D4"
id="stop4" />
</linearGradient>
<polygon
class="st3"
points="29.2,51.8 69.5,47 69.8,209.1 29.2,204.4 "
id="polygon4" />
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="151.9309"
y1="42.7213"
x2="142.8473"
y2="-43.5726"
gradientTransform="matrix(0.9941 1.431752e-03 1.431754e-03 1.1143 -3.0394 44.4335)">
<stop
offset="0"
style="stop-color:#B3B3B3"
id="stop5" />
<stop
offset="0.4752"
style="stop-color:#B5B5B5"
id="stop6" />
<stop
offset="0.6464"
style="stop-color:#BCBCBC"
id="stop7" />
<stop
offset="0.7685"
style="stop-color:#C7C7C7"
id="stop8" />
<stop
offset="0.8671"
style="stop-color:#D8D8D8"
id="stop9" />
<stop
offset="0.9506"
style="stop-color:#EEEEEE"
id="stop10" />
<stop
offset="1"
style="stop-color:#FFFFFF"
id="stop11" />
</linearGradient>
<polygon
class="st4"
points="219.3,98.5 97.4,93.2 69.5,47.3 218.9,55.6 "
id="polygon11" />
<polygon
class="st1"
points="102,85.3 251.2,93 252.8,91.1 72.2,48.9 69.5,47 "
id="polygon12" />
<polygon
class="st5"
points="252.8,91.1 128,84.6 102,82.9 69.8,47.3 219.1,55.6 233.6,71.4 252.3,90.6 252.3,90.6 "
id="polygon13" />
<radialGradient
id="SVGID_4_"
cx="445.2994"
cy="-436.338"
r="4.0179"
gradientTransform="matrix(0.5088 -4.329579e-03 0.1464 14.7395 -92.0455 6569.5317)"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop13" />
<stop
offset="6.758273e-02"
style="stop-color:#FFFFFF;stop-opacity:0.9324"
id="stop14" />
<stop
offset="1"
style="stop-color:#FFFFFF;stop-opacity:0"
id="stop15" />
</radialGradient>
<path
class="st6"
d="M72.2,152.5c0.2,26.2,0.9,42.4,0.1,42.4c-0.9,0-1.5-6.3-2.5-32.3c-1.1-26.1-1.4-85-0.5-85.1 C70.1,77.2,71.9,126.4,72.2,152.5z"
id="path15" />
<g
class="st7"
id="g17">
<path
class="st8"
d="M29.1,203.9l20.4,2.1c3.3,0.4,6.9,0.6,10.2,1.1l10.2,1.2h-0.1l74.7-3.2l37.4-1.7l9.3-0.4 c3.1-0.1,6.3-0.2,9.3-0.4l18.7-0.5l-18.7,1.2c-3.1,0.2-6.3,0.4-9.3,0.5l-9.3,0.4l-37.4,1.7l-74.5,3.2l0,0l0,0L59.7,208 c-3.3-0.4-6.8-0.9-10.2-1.4L29.1,203.9z"
id="path16" />
<path
class="st1"
d="M28.6,203.9c3.3,0.2,6.8,0.4,10.3,0.6s7.1,0.5,10.6,0.9l10.2,1.1l10.2,1.2l-0.1,1.1h-0.1v-1.1l74.8-3.1 l37.4-1.6l18.7-0.7l18.7-0.5v0.6l-18.7,1.1l-9.3,0.5l-9.3,0.4l-37.4,1.6l-74.7,3.1l0,0l0,0l-10.2-1.2l-10.2-1.4L29,203.8 L28.6,203.9z M30.3,204.1l19.2,2.5l10.2,1.4l10.2,1.2l0,0l74.7-3.3l37.4-1.7l9.3-0.4l9.3-0.5l18.7-1.2v0.6l-18.7,0.5l-18.7,0.7 l-37.4,1.7l-74.7,3.3v-1.1h0.1l-0.1,1.1l-10.2-1.2l-10.2-1.1c-3.3-0.4-6.5-0.6-9.7-1.1C36.6,205,33.5,204.5,30.3,204.1z"
id="path17" />
</g>
<g
id="g28">
<g
id="g27">
<g
id="g20">
<path
class="st9"
d="M181.4,136.4c-8.7,6.8-23.5,8.1-33.8,5.5c2.6-2.3,3.8-3.4,6.3-5.8c2.5-2.2,3.6-3.2,6-5.4 c8.4-7.4,12.5-10.8,20.7-17.7c-8.5,6.4-12.9,9.6-21.6,16.4c-2.5,2-3.7,2.8-6.1,4.8c-2.6,2-3.8,3.1-6.4,5 c-0.5-9.5,1.1-22.1,10.3-28.9c0.7-0.6,1.7-1.1,2.6-1.7c1.2-0.6,2.5-1.4,3.9-1.8c11.4-4.4,24.8-7.5,37.3-5.9 c0.7,6.5-4.9,18.9-11.8,28.2c-1,1.2-1.8,2.5-2.8,3.6C184.2,133.9,182.7,135.3,181.4,136.4z"
id="path18"
style="fill:#ab60e3;fill-opacity:1" />
<path
class="st10"
d="M185.6,132.4c-9.2,6-22.6,5.8-31.7,3.7c2.5-2.2,3.6-3.2,6-5.4c8.4-7.4,12.5-10.8,20.7-17.7 c-8.5,6.4-12.9,9.6-21.6,16.4c-2.5,2-3.7,2.8-6.1,4.8c-0.5-7.9,0.4-18.4,6.5-25.5c1.2-0.6,2.5-1.4,3.9-1.8 c11.4-4.6,24.8-7.5,37.3-5.9c0.7,6.5-4.9,18.9-11.8,28.2C187.5,130.1,186.5,131.3,185.6,132.4z"
id="path19"
style="fill:#8038b8;fill-opacity:1" />
<path
class="st11"
d="M188.5,128.9c-8.9,4.2-20.5,3.8-28.5,1.8c8.4-7.4,12.5-10.8,20.7-17.7c-8.5,6.4-12.9,9.6-21.6,16.4 c-0.5-6.8,0-15.7,4.3-22.6c11.4-4.4,24.8-7.5,37.3-5.9C201.2,107.4,195.5,119.9,188.5,128.9z"
id="path20"
style="fill:#560a8f;fill-opacity:1" />
</g>
<g
id="g23">
<path
class="st12"
d="M140.4,169.2c-3.6-8.9,0.5-19.6,4.7-26c1.1,2.5,1.6,3.7,2.7,6c1.1,2.3,1.6,3.4,2.6,5.7 c3.7,7.9,5.5,11.8,9.3,19.2c-3.1-7.6-4.7-11.6-7.9-19.7c-0.9-2.2-1.4-3.3-2.2-5.5c-1-2.3-1.5-3.6-2.3-6 c7.4,2.2,16.8,6.6,20.3,15c0.2,0.7,0.5,1.5,0.7,2.2c0.2,1,0.5,2.1,0.6,3.2c1.5,9.6-0.9,23-4.4,28c-5.5-0.9-14.5-7.7-20-15.1 c-0.7-1-1.5-2-2.1-3C141.7,171.9,141,170.6,140.4,169.2z"
id="path21"
style="fill:#bb9dd2;fill-opacity:1" />
<path
class="st13"
d="M142.5,173.3c-2.3-8.4,1.5-18.1,5.4-24c1.1,2.3,1.6,3.4,2.6,5.7c3.7,7.9,5.5,11.8,9.3,19.2 c-3.1-7.6-4.7-11.6-7.9-19.7c-0.9-2.2-1.4-3.3-2.2-5.5c6.3,1.7,14.4,5.2,18.7,11.3c0.2,1,0.5,2.1,0.6,3.2 c1.5,9.6-0.9,23-4.4,27.9c-5.5-0.9-14.5-7.7-20-15.1C143.9,175.2,143.3,174.3,142.5,173.3z"
id="path22"
style="fill:#9a6cbc;fill-opacity:1" />
<path
class="st14"
d="M144.6,176.2c-1.1-7.5,2.5-16,5.9-21.3c3.7,7.9,5.5,11.8,9.3,19.2c-3.1-7.6-4.7-11.6-7.9-19.7 c5.5,1.4,12.5,4.1,17.2,8.9c1.5,9.6-0.9,23-4.4,27.9C159,190.4,150.1,183.6,144.6,176.2z"
id="path23"
style="fill:#783ba5;fill-opacity:1" />
</g>
<g
id="g26">
<path
class="st15"
d="M125.9,116.6c10.5,4.3,16.5,15.4,18.8,23.4c-3-1-4.3-1.4-7.3-2.3c-2.8-0.9-4.2-1.4-6.9-2.2 c-9.7-3.2-14.5-4.9-23.9-8.2c9.1,3.8,13.7,5.8,23.1,9.6c2.6,1.1,3.9,1.6,6.5,2.7c2.7,1.1,4.1,1.6,6.9,2.7 c-7.4,4.2-18.4,8.4-28.3,4.6c-0.7-0.2-1.7-0.7-2.6-1.2c-1-0.6-2.2-1.2-3.3-2.1c-8.5-6-17.6-16.7-20.9-26.8 c4.9-3.4,17.6-4.2,28.3-2.3c1.5,0.2,2.8,0.5,4.3,0.9C122.7,115.4,124.3,115.8,125.9,116.6z"
id="path24"
style="fill:#ab60e3;fill-opacity:1" />
<path
class="st16"
d="M120.7,114.9c9.1,4.8,14.5,15,16.7,22.6c-2.8-0.9-4.2-1.4-6.9-2.2c-9.7-3.2-14.5-4.9-23.9-8.2 c9.1,3.8,13.7,5.8,23,9.6c2.6,1.1,3.9,1.6,6.5,2.7c-6.1,3.6-15.4,7.4-23.9,6c-1-0.6-2.2-1.2-3.3-2.1c-8.5-6-17.6-16.7-20.9-26.8 c4.9-3.4,17.6-4.2,28.3-2.3C118,114.2,119.4,114.4,120.7,114.9z"
id="path25"
style="fill:#8038b8;fill-opacity:1" />
<path
class="st17"
d="M116.6,113.9c7.5,5.3,12.1,14.4,14,21.3c-9.7-3.2-14.5-4.9-23.9-8.2c9.1,3.8,13.7,5.8,23.1,9.6 c-5.4,3.2-13,6.6-20.7,6.5c-8.5-6-17.6-16.7-20.9-26.8C93.2,112.8,105.7,112,116.6,113.9z"
id="path26"
style="fill:#6f2796;fill-opacity:1" />
</g>
</g>
</g>
<linearGradient
id="SVGID_5_"
gradientUnits="userSpaceOnUse"
x1="241.7537"
y1="104.2354"
x2="160.0455"
y2="55.1756"
gradientTransform="matrix(1 0 0 -1 0 256)">
<stop
offset="0.1721"
style="stop-color:#C7C7C7"
id="stop28" />
<stop
offset="0.3798"
style="stop-color:#D8D8D8"
id="stop29" />
<stop
offset="0.6814"
style="stop-color:#DADADA"
id="stop30" />
<stop
offset="0.7898"
style="stop-color:#E1E1E1"
id="stop31" />
<stop
offset="0.867"
style="stop-color:#ECECEC"
id="stop32" />
<stop
offset="0.8745"
style="stop-color:#EEEEEE"
id="stop33" />
<stop
offset="1"
style="stop-color:#FFFFFF"
id="stop34" />
</linearGradient>
<path
class="st18"
d="M219.1,128.3c-1,0.4-3.3,15.7-3.7,19.2c-0.7,5.8-3.9,28.7-11.1,41.2c-7.3,12.8-15.7,13.7-16.4,14.6l31.1-0.9 C219.1,179.1,219.1,151.5,219.1,128.3L219.1,128.3z"
id="path34" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 9.7 KiB

View File

@@ -65,7 +65,7 @@
"toggle-image-properties": "Bildattribute umschalten",
"toggle-owned-attributes": "Eigene Attribute umschalten",
"toggle-inherited-attributes": "Vererbte Attribute umschalten",
"toggle-promoted-attributes": "Hervorgehobene Attribute umschalten",
"toggle-promoted-attributes": "Beworbene Attribute umschalten",
"toggle-link-map": "Link-Karte umschalten",
"toggle-note-info": "Notizinformationen umschalten",
"toggle-note-paths": "Notizpfade umschalten",
@@ -391,7 +391,7 @@
"toggle-ribbon-tab-image-properties": "Registerkarte Bilder-Eigenschaften umschalten",
"toggle-ribbon-tab-owned-attributes": "Registerkarte Besitzerattribute umschalten",
"toggle-ribbon-tab-inherited-attributes": "Registerkarte geerbte Attribute umschalten",
"toggle-ribbon-tab-promoted-attributes": "Registerkarte hervorgehobene Attribute umschalten",
"toggle-ribbon-tab-promoted-attributes": "Registerkarte verliehene Attribute umschalten",
"toggle-ribbon-tab-note-map": "Registerkarte Notizkarte umschalten",
"toggle-ribbon-tab-note-info": "Registerkarte Notiz-Info umschalten",
"toggle-ribbon-tab-note-paths": "Registerkarte Notiz-Pfad umschalten",

View File

@@ -356,8 +356,7 @@
"visible-launchers-title": "Visible Launchers",
"user-guide": "User Guide",
"localization": "Language & Region",
"inbox-title": "Inbox",
"tab-switcher-title": "Tab Switcher"
"inbox-title": "Inbox"
},
"notes": {
"new-note": "New note",

View File

@@ -293,7 +293,7 @@
"migration": {
"old_version": "La migración directa desde tu versión actual no está soportada. Por favor actualice a v0.60.4 primero y solo después a esta versión.",
"error_message": "Error durante la migración a la versión {{version}}: {{stack}}",
"wrong_db_version": "La versión de la base de datos {{version}} es más nueva de lo que la aplicación espera {{targetVersion}}, lo que significa que fue creada por una versión más reciente e incompatible de Trilium. Actualice a la última versión de Trilium para resolver este problema."
"wrong_db_version": "La versión de la DB {{version}} es más nueva que la versión de la DB actual {{targetVersion}}, lo que significa que fue creada por una versión más reciente e incompatible de Trilium. Actualice a la última versión de Trilium para resolver este problema."
},
"modals": {
"error_title": "Error"

View File

@@ -25,8 +25,7 @@
"login": {
"title": "Logg inn",
"password": "Passord",
"button": "Logg inn",
"remember-me": "Husk meg"
"button": "Logg inn"
},
"setup_sync-from-server": {
"server-host-placeholder": "https://<hostnavn>:<port>",

View File

@@ -8,12 +8,6 @@ import type { AttachmentRow } from "@triliumnext/commons";
import type { ValidatorMap } from "./etapi-interface.js";
function register(router: Router) {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments();
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
});
const ALLOWED_PROPERTIES_FOR_CREATE_ATTACHMENT: ValidatorMap = {
ownerId: [v.notNull, v.isNoteId],
role: [v.notNull, v.isString],

View File

@@ -121,16 +121,6 @@ function getAndCheckAttribute(attributeId: string) {
}
}
function getAndCheckRevision(revisionId: string) {
const revision = becca.getRevision(revisionId);
if (revision) {
return revision;
} else {
throw new EtapiError(404, "REVISION_NOT_FOUND", `Revision '${revisionId}' not found.`);
}
}
function validateAndPatch(target: any, source: any, allowedProperties: ValidatorMap) {
for (const key of Object.keys(source)) {
if (!(key in allowedProperties)) {
@@ -162,6 +152,5 @@ export default {
getAndCheckNote,
getAndCheckBranch,
getAndCheckAttribute,
getAndCheckAttachment,
getAndCheckRevision
getAndCheckAttachment
};

View File

@@ -2,7 +2,6 @@ import type BAttachment from "../becca/entities/battachment.js";
import type BAttribute from "../becca/entities/battribute.js";
import type BBranch from "../becca/entities/bbranch.js";
import type BNote from "../becca/entities/bnote.js";
import type BRevision from "../becca/entities/brevision.js";
function mapNoteToPojo(note: BNote) {
return {
@@ -65,28 +64,9 @@ function mapAttachmentToPojo(attachment: BAttachment) {
};
}
function mapRevisionToPojo(revision: BRevision) {
return {
revisionId: revision.revisionId,
noteId: revision.noteId,
type: revision.type,
mime: revision.mime,
isProtected: revision.isProtected,
title: revision.title,
blobId: revision.blobId,
dateLastEdited: revision.dateLastEdited,
dateCreated: revision.dateCreated,
utcDateLastEdited: revision.utcDateLastEdited,
utcDateCreated: revision.utcDateCreated,
utcDateModified: revision.utcDateModified,
contentLength: revision.contentLength
};
}
export default {
mapNoteToPojo,
mapBranchToPojo,
mapAttributeToPojo,
mapAttachmentToPojo,
mapRevisionToPojo
mapAttachmentToPojo
};

View File

@@ -1,205 +0,0 @@
import becca from "../becca/becca.js";
import sql from "../services/sql.js";
import eu from "./etapi_utils.js";
import mappers from "./mappers.js";
import noteService from "../services/notes.js";
import TaskContext from "../services/task_context.js";
import protectedSessionService from "../services/protected_session.js";
import utils from "../services/utils.js";
import type { Router } from "express";
import type { NoteRow, RecentChangeRow } from "@triliumnext/commons";
function register(router: Router) {
// GET /etapi/notes/history - must be registered before /etapi/notes/:noteId routes
eu.route(router, "get", "/etapi/notes/history", (req, res, next) => {
const ancestorNoteId = (req.query.ancestorNoteId as string) || "root";
let recentChanges: RecentChangeRow[];
if (ancestorNoteId === "root") {
// Optimized path: no ancestor filtering needed, fetch directly from DB
recentChanges = sql.getRows<RecentChangeRow>(`
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
revisions.title,
revisions.utcDateCreated AS utcDate,
revisions.dateCreated AS date
FROM revisions
JOIN notes USING(noteId)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS utcDate,
notes.dateCreated AS date
FROM notes
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM notes
WHERE notes.isDeleted = 1
ORDER BY utcDate DESC
LIMIT 500`);
} else {
// Use recursive CTE to find all descendants, then filter at DB level
// This pushes filtering to the database for much better performance
recentChanges = sql.getRows<RecentChangeRow>(`
WITH RECURSIVE descendants(noteId) AS (
SELECT ?
UNION
SELECT branches.noteId
FROM branches
JOIN descendants ON branches.parentNoteId = descendants.noteId
)
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
revisions.title,
revisions.utcDateCreated AS utcDate,
revisions.dateCreated AS date
FROM revisions
JOIN notes USING(noteId)
WHERE notes.noteId IN (SELECT noteId FROM descendants)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateCreated AS utcDate,
notes.dateCreated AS date
FROM notes
WHERE notes.noteId IN (SELECT noteId FROM descendants)
UNION ALL
SELECT
notes.noteId,
notes.isDeleted AS current_isDeleted,
notes.deleteId AS current_deleteId,
notes.title AS current_title,
notes.isProtected AS current_isProtected,
notes.title,
notes.utcDateModified AS utcDate,
notes.dateModified AS date
FROM notes
WHERE notes.isDeleted = 1 AND notes.noteId IN (SELECT noteId FROM descendants)
ORDER BY utcDate DESC
LIMIT 500`, [ancestorNoteId]);
}
for (const change of recentChanges) {
if (change.current_isProtected) {
if (protectedSessionService.isProtectedSessionAvailable()) {
change.title = protectedSessionService.decryptString(change.title) || "[protected]";
change.current_title = protectedSessionService.decryptString(change.current_title) || "[protected]";
} else {
change.title = change.current_title = "[protected]";
}
}
if (change.current_isDeleted) {
const deleteId = change.current_deleteId;
const undeletedParentBranchIds = noteService.getUndeletedParentBranchIds(change.noteId, deleteId);
// note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op)
change.canBeUndeleted = undeletedParentBranchIds.length > 0;
}
}
res.json(recentChanges);
});
// GET /etapi/notes/:noteId/revisions - List all revisions for a note
eu.route(router, "get", "/etapi/notes/:noteId/revisions", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId);
const revisions = becca.getRevisionsFromQuery(
`SELECT revisions.*, LENGTH(blobs.content) AS contentLength
FROM revisions
JOIN blobs USING (blobId)
WHERE noteId = ?
ORDER BY utcDateCreated DESC`,
[note.noteId]
);
res.json(revisions.map((revision) => mappers.mapRevisionToPojo(revision)));
});
// POST /etapi/notes/:noteId/undelete - Restore a deleted note
eu.route(router, "post", "/etapi/notes/:noteId/undelete", (req, res, next) => {
const { noteId } = req.params;
const noteRow = sql.getRow<NoteRow | null>("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (!noteRow) {
throw new eu.EtapiError(404, "NOTE_NOT_FOUND", `Note '${noteId}' not found.`);
}
if (!noteRow.isDeleted || !noteRow.deleteId) {
throw new eu.EtapiError(400, "NOTE_NOT_DELETED", `Note '${noteId}' is not deleted.`);
}
const undeletedParentBranchIds = noteService.getUndeletedParentBranchIds(noteId, noteRow.deleteId);
if (undeletedParentBranchIds.length === 0) {
throw new eu.EtapiError(400, "CANNOT_UNDELETE", `Cannot undelete note '${noteId}' - no undeleted parent found.`);
}
const taskContext = new TaskContext("no-progress-reporting", "undeleteNotes", null);
noteService.undeleteNote(noteId, taskContext);
res.json({ success: true });
});
// GET /etapi/revisions/:revisionId - Get revision metadata
eu.route(router, "get", "/etapi/revisions/:revisionId", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
throw new eu.EtapiError(400, "REVISION_IS_PROTECTED", `Revision '${req.params.revisionId}' is protected and cannot be read through ETAPI.`);
}
res.json(mappers.mapRevisionToPojo(revision));
});
// GET /etapi/revisions/:revisionId/content - Get revision content
eu.route(router, "get", "/etapi/revisions/:revisionId/content", (req, res, next) => {
const revision = eu.getAndCheckRevision(req.params.revisionId);
if (revision.isProtected) {
throw new eu.EtapiError(400, "REVISION_IS_PROTECTED", `Revision '${req.params.revisionId}' is protected and content cannot be read through ETAPI.`);
}
const filename = utils.formatDownloadTitle(revision.title, revision.type, revision.mime);
res.setHeader("Content-Disposition", utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Content-Type", revision.mime);
res.send(revision.getContent());
});
}
export default {
register
};

View File

@@ -47,10 +47,10 @@ async function register(app: express.Application) {
vite.middlewares(req, res, next);
});
app.get(`/`, [ rootLimiter, auth.checkAuth, csrfMiddleware ], (req, res, next) => {
req.url = `/${assetUrlFragment}/index.html`;
req.url = `/${assetUrlFragment}/src/index.html`;
vite.middlewares(req, res, next);
});
app.get(`/src/index.ts`, [ rootLimiter ], (req, res, next) => {
app.get(`/index.ts`, [ rootLimiter ], (req, res, next) => {
req.url = `/${assetUrlFragment}/src/index.ts`;
vite.middlewares(req, res, next);
});
@@ -66,7 +66,7 @@ async function register(app: express.Application) {
// broken when closing the browser and coming back in to the page.
// The page is restored from cache, but the API call fail.
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.sendFile(path.join(publicDir, "index.html"));
res.sendFile(path.join(publicDir, "src", "index.html"));
});
app.use("/assets", persistentCacheStatic(path.join(publicDir, "assets")));
app.use(`/src`, persistentCacheStatic(path.join(publicDir, "src")));

View File

@@ -12,7 +12,7 @@ import log from "../services/log.js";
import optionService from "../services/options.js";
import protectedSessionService from "../services/protected_session.js";
import sql from "../services/sql.js";
import { isDev, isElectron, isMac, isWindows11 } from "../services/utils.js";
import { isDev, isElectron, isWindows11 } from "../services/utils.js";
import { generateToken as generateCsrfToken } from "./csrf_protection.js";
@@ -43,10 +43,7 @@ export function bootstrap(req: Request, res: Response) {
platform: process.platform,
isElectron,
hasNativeTitleBar: isElectron && nativeTitleBarVisible,
hasBackgroundEffects: options.backgroundEffects === "true"
&& isElectron
&& (isWindows11 || isMac)
&& !nativeTitleBarVisible,
hasBackgroundEffects: isElectron && isWindows11 && !nativeTitleBarVisible && options.backgroundEffects === "true",
maxEntityChangeIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes"),
maxEntityChangeSyncIdAtLoad: sql.getValue("SELECT COALESCE(MAX(id), 0) FROM entity_changes WHERE isSynced = 1"),
instanceName: config.General ? config.General.instanceName : null,

View File

@@ -12,7 +12,6 @@ import etapiMetricsRoute from "../etapi/metrics.js";
import etapiNoteRoutes from "../etapi/notes.js";
import etapiSpecRoute from "../etapi/spec.js";
import etapiSpecialNoteRoutes from "../etapi/special_notes.js";
import etapiRevisionsRoutes from "../etapi/revisions.js";
import auth from "../services/auth.js";
import openID from '../services/open_id.js';
import { isElectron } from "../services/utils.js";
@@ -362,8 +361,6 @@ function register(app: express.Application) {
etapiAttachmentRoutes.register(router);
etapiAttributeRoutes.register(router);
etapiBranchRoutes.register(router);
// Register revisions routes BEFORE notes routes so /etapi/notes/history is matched before /etapi/notes/:noteId
etapiRevisionsRoutes.register(router);
etapiNoteRoutes.register(router);
etapiSpecialNoteRoutes.register(router);
etapiSpecRoute.register(router);

View File

@@ -48,7 +48,7 @@ export default function buildLaunchBarConfig() {
id: "_lbBackInHistory",
...sharedLaunchers.backInHistory
},
{
{
id: "_lbForwardInHistory",
...sharedLaunchers.forwardInHistory
},
@@ -59,12 +59,12 @@ export default function buildLaunchBarConfig() {
command: "commandPalette",
icon: "bx bx-chevron-right-square"
},
{
{
id: "_lbBackendLog",
title: t("hidden-subtree.backend-log-title"),
type: "launcher",
targetNoteId: "_backendLog",
icon: "bx bx-detail"
icon: "bx bx-detail"
},
{
id: "_zenMode",
@@ -128,7 +128,7 @@ export default function buildLaunchBarConfig() {
baseSize: "50",
growthFactor: "0"
},
{
{
id: "_lbBookmarks",
title: t("hidden-subtree.bookmarks-title"),
type: "launcher",
@@ -139,7 +139,7 @@ export default function buildLaunchBarConfig() {
id: "_lbToday",
...sharedLaunchers.openToday
},
{
{
id: "_lbSpacer2",
title: t("hidden-subtree.spacer-title"),
type: "launcher",
@@ -179,11 +179,7 @@ export default function buildLaunchBarConfig() {
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
{ id: "_lbMobileToday", ...sharedLaunchers.openToday },
{
id: "_lbMobileRecentChanges",
...sharedLaunchers.recentChanges
}
{ id: "_lbMobileToday", ...sharedLaunchers.openToday }
];
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
@@ -207,10 +203,8 @@ export default function buildLaunchBarConfig() {
...sharedLaunchers.calendar
},
{
id: "_lbMobileTabSwitcher",
title: t("hidden-subtree.tab-switcher-title"),
type: "launcher",
builtinWidget: "mobileTabSwitcher"
id: "_lbMobileRecentChanges",
...sharedLaunchers.recentChanges
}
];
@@ -220,4 +214,4 @@ export default function buildLaunchBarConfig() {
mobileAvailableLaunchers,
mobileVisibleLaunchers
};
}
}

View File

@@ -4,7 +4,6 @@ import { t } from "i18next";
import path from "path";
import url from "url";
import app_info from "./app_info.js";
import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js";
import log from "./log.js";
@@ -237,19 +236,17 @@ function getWindowExtraOpts() {
// Linux or other platforms.
extraOpts.frame = false;
}
}
// Window effects (Mica on Windows and Vibrancy on macOS)
// These only work if native title bar is not enabled.
if (optionService.getOptionBool("backgroundEffects")) {
if (isMac) {
extraOpts.transparent = true;
extraOpts.visualEffectState = "active";
} else if (isWindows) {
extraOpts.backgroundMaterial = "auto";
} else {
// Linux or other platforms.
extraOpts.transparent = true;
}
// Window effects (Mica)
if (optionService.getOptionBool("backgroundEffects")) {
if (isMac) {
// Vibrancy not yet supported.
} else if (isWindows) {
extraOpts.backgroundMaterial = "auto";
} else {
// Linux or other platforms.
extraOpts.transparent = true;
}
}
@@ -293,9 +290,6 @@ function getIcon() {
if (process.env.NODE_ENV === "development") {
return path.join(__dirname, "../../../desktop/electron-forge/app-icon/png/256x256-dev.png");
}
if (app_info.appVersion.includes("test")) {
return path.join(RESOURCE_DIR, "../public/assets/icon-dev.png");
}
return path.join(RESOURCE_DIR, "../public/assets/icon.png");
}

View File

@@ -28,7 +28,6 @@ export default defineConfig(() => ({
provider: 'v8' as const,
reporter: [ "text", "html" ]
},
pool: "vmForks",
maxWorkers: 4
pool: "vmForks"
},
}));

View File

@@ -69,7 +69,7 @@ export default defineContentScript({
}
function getRectangleArea() {
return new Promise<Rect | null>((resolve) => {
return new Promise<Rect>((resolve) => {
const overlay = document.createElement('div');
overlay.style.opacity = '0.6';
overlay.style.background = 'black';
@@ -177,7 +177,6 @@ export default defineContentScript({
console.info('selectionArea:', selectionArea);
if (!selectionArea || !selectionArea.width || !selectionArea.height) {
resolve(null);
return;
}
@@ -190,7 +189,6 @@ export default defineContentScript({
function cancel(event: KeyboardEvent) {
if (event.key === "Escape") {
removeOverlay();
resolve(null);
}
}
@@ -338,20 +336,14 @@ export default defineContentScript({
}
}
browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
(async () => {
try {
const response = await prepareMessageResponse(message);
sendResponse(response);
} catch (err) {
console.error(err);
sendResponse(undefined);
}
})();
// Critical for async responses in Chrome MV2
return true;
browser.runtime.onMessage.addListener(async (message) => {
try {
const response = await prepareMessageResponse(message);
return response;
} catch (err) {
console.error(err);
throw err;
}
});
}
});

View File

@@ -13,7 +13,7 @@
"postinstall": "wxt prepare"
},
"keywords": [],
"packageManager": "pnpm@10.28.2",
"packageManager": "pnpm@10.28.1",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.0",
"wxt": "0.20.13"

View File

@@ -11,13 +11,13 @@
"dependencies": {
"i18next": "25.8.0",
"i18next-http-backend": "3.0.2",
"preact": "10.28.3",
"preact": "10.28.2",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.5",
"react-i18next": "16.5.4"
"react-i18next": "16.5.3"
},
"devDependencies": {
"@preact/preset-vite": "2.10.3",
"@preact/preset-vite": "2.10.2",
"eslint": "9.39.2",
"eslint-config-preact": "2.0.0",
"typescript": "5.9.3",

View File

@@ -21,7 +21,7 @@
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
"attributes_description": "Nutze Beziehungen zwischen Notizen oder füge Label hinzu um diese einfach zu kategorisieren. Verwende hervorgehobene Attribute um strukturierte Informationen zu hinterlegen die in Tabellen oder Boards verwendet werden."
"attributes_description": "Für leichtes kategorsieren, nutze Verbindungen zwischen Notizen oder füge Label hinzu. Verwende hervorgehobene Attribute, um sie als strukturierte Informationen in Tabellen oder Anschlagbretter zu verwenden."
},
"productivity_benefits": {
"revisions_title": "Notizrevisionen",

View File

@@ -31,12 +31,7 @@
"sync_content": "Gunakan hostinganmu sendiri atau instansi cloud untuk sinkronisasi mudah catatan-catatan anda pada beberapa perangkat, dan untuk akses dari ponsel anda dengan PWA.",
"search_content": "Atau cari teks di dalam catatan lalu lebih dalam dengan cari catatan induk, atau berdasarkan kedalaman.",
"web_clipper_title": "Penyemat Web",
"web_clipper_content": "Mengambil halaman web (atau foto halaman web) dan disematkan langsung ke catatan Trilium dengan ekstensi browser penyemat web.",
"protected_notes_title": "Catatan terlindungi",
"protected_notes_content": "Lindungi informasi pribadi sensitif dengan mengenkripsi catatan dan menguncinya di balik sesi yang dilindungi kata sandi.",
"jump_to_title": "Pencarian cepat dan perintah",
"jump_to_content": "Melompat dengan cepat ke catatan atau perintah UI di seluruh hierarki dengan mencari judulnya, dengan pencocokan kabur untuk memperhitungkan kesalahan ketik atau perbedaan kecil.",
"search_title": "Pencarian mumpuni"
"web_clipper_content": "Mengambil halaman web (atau foto halaman web) dan disematkan langsung ke catatan Trilium dengan ekstensi browser penyemat web."
},
"note_types": {
"title": "Cara-cara menampilkan informasi Anda",
@@ -70,8 +65,5 @@
"database_question": "Di manakah data disimpan?",
"database_answer": "Semua catatan Anda akan disimpan dalam basis data SQLite di dalam sebuah folder aplikasi. Alasan mengapa Trilium menggunakan basis data alih-alih file teks biasa adalah demi performa dan karena beberapa fitur akan jauh lebih sulit untuk diterapkan, seperti klon (catatan yang sama di beberapa tempat dalam hierarki). Untuk menemukan folder aplikasinya, cukup buka jendela 'Tentang'.",
"server_question": "Apakah saya butuh server untuk menjalankan Trilium?"
},
"extensibility_benefits": {
"share_title": "Bagikan catatan di web"
}
}

View File

@@ -49,17 +49,13 @@
"title": "Кілька способів представлення вашої інформації",
"canvas_description": "Розташовуйте фігури, зображення та текст на нескінченному полотні, використовуючи ту саму технологію, що й excalidraw.com. Ідеально підходить для діаграм, ескізів та візуального планування.",
"mermaid_description": "Створюйте діаграми, такі як блок-схеми, діаграми класів та послідовностей, діаграми Ганта та багато іншого, використовуючи синтаксис Mermaid.",
"others_list": "та інші: <0>карта нотаток</0>, <1>карта зв'язків</1>, <2>збережені пошуки</2>, <3>візуалізація нотаток</3> та <4>веб-перегляди</4>.",
"mermaid_title": "Mermaid діаграми"
"others_list": "та інші: <0>карта нотаток</0>, <1>карта зв'язків</1>, <2>збережені пошуки</2>, <3>візуалізація нотаток</3> та <4>веб-перегляди</4>."
},
"extensibility_benefits": {
"title": "Спільне використання та розширюваність",
"import_export_title": "Імпорт/експорт",
"import_export_description": "Легко взаємодійте з іншими програмами, використовуючи формати Markdown, ENEX, OML.",
"share_title": "Діліться нотатками в Інтернеті",
"share_description": "Якщо у Вас є сервер, Ви можете використати його, щоб поділитися частиною своїх нотаток з іншими людьми.",
"api_title": "REST API",
"api_description": "Взаємодійте з Trilium програмно, використовуючи його вбудований REST API."
"share_title": "Діліться нотатками в Інтернеті"
},
"collections": {
"title": "Колекції",
@@ -156,26 +152,6 @@
"description_x64": "Для більшості дистрибутивів Linux, сумісних з архітектурою x86_64.",
"description_arm64": "Для дистрибутивів Linux на базі ARM, сумісних з архітектурою aarch64.",
"quick_start": "Виберіть відповідний формат пакета, залежно від вашого дистрибутива:",
"download_deb": ".deb",
"download_rpm": ".rpm",
"download_flatpak": ".flatpak",
"download_nixpkgs": "nixpkgs"
},
"download_helper_desktop_macos": {
"title_x64": "macOS для Intel",
"title_arm64": "macOS для Apple Silicon",
"quick_start": "Для того, щоб встановити за допомогою Homebrew:",
"download_homebrew_cask": "Homebrew Cask"
},
"download_helper_server_docker": {
"download_dockerhub": "Docker Hub",
"download_ghcr": "ghcr.io"
},
"download_helper_server_linux": {
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)"
},
"download_helper_server_hosted": {
"title": "Платний хостинг"
"download_deb": ".deb"
}
}

26
docs/README-nb_NO.md vendored
View File

@@ -76,21 +76,21 @@ Vår dokumentasjon er tilgjengelig i flere format:
* Support for editing [notes with source
code](https://docs.triliumnotes.org/user-guide/note-types/code), including
syntax highlighting
* Rask og enkel [navigering mellom
notater](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
fulltekstsøk og
[notat-fokusering](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* Fast and easy [navigation between
notes](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-navigation),
full text search and [note
hoisting](https://docs.triliumnotes.org/user-guide/concepts/navigation/note-hoisting)
* Sømløs
[notathistorikk](https://docs.triliumnotes.org/user-guide/concepts/notes/note-revisions)
* Notaters
[attributter](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes)
kan brukes til å organisere notater, utføre spørringer og avansert
[skripting](https://docs.triliumnotes.org/user-guide/scripts)
* Brukergrensesnitt tilgjengelig på Engelsk, Tysk, Spansk, Fransk, Rumensk, og
Kinesisk (forenklet og tradisjonell)
* Direkte [OpenID og
TOTP-integrasjon](https://docs.triliumnotes.org/user-guide/setup/server/mfa)
for sikrere logging
* Note
[attributes](https://docs.triliumnotes.org/user-guide/advanced-usage/attributes)
can be used for note organization, querying and advanced
[scripting](https://docs.triliumnotes.org/user-guide/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese
(simplified and traditional)
* Direct [OpenID and TOTP
integration](https://docs.triliumnotes.org/user-guide/setup/server/mfa) for
more secure login
* [Synkronisering](https://docs.triliumnotes.org/user-guide/setup/synchronization)
med selv-hostet sync server
* there are [3rd party services for hosting synchronisation

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