diff --git a/SECURITY.md b/SECURITY.md index 0c7e17bd6e..20c58fca16 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,4 +10,5 @@ Description above is a general rule and may be altered on case by case basis. ## Reporting a Vulnerability -You can report low severity vulnerabilities as GitHub issues, more severe vulnerabilities should be reported to the email [contact@eliandoran.me](mailto:contact@eliandoran.me) +* For low severity vulnerabilities, they can be reported as GitHub issues. +* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories). diff --git a/apps/build-docs/package.json b/apps/build-docs/package.json index 99b10e5b3c..934f610b9e 100644 --- a/apps/build-docs/package.json +++ b/apps/build-docs/package.json @@ -1,22 +1,28 @@ { "name": "build-docs", "version": "1.0.0", - "description": "", + "description": "Build documentation from Trilium notes", "main": "src/main.ts", + "bin": { + "trilium-build-docs": "dist/cli.js" + }, "scripts": { - "start": "tsx ." + "start": "tsx .", + "cli": "tsx src/cli.ts", + "build": "tsx scripts/build.ts" }, "keywords": [], "author": "Elian Doran ", "license": "AGPL-3.0-only", - "packageManager": "pnpm@10.29.3", + "packageManager": "pnpm@10.30.1", "devDependencies": { - "@redocly/cli": "2.18.0", + "@redocly/cli": "2.19.1", "archiver": "7.0.1", "fs-extra": "11.3.3", + "js-yaml": "4.1.1", "react": "19.2.4", "react-dom": "19.2.4", - "typedoc": "0.28.16", + "typedoc": "0.28.17", "typedoc-plugin-missing-exports": "4.1.2" } } diff --git a/apps/build-docs/scripts/build.ts b/apps/build-docs/scripts/build.ts new file mode 100644 index 0000000000..79fa01f3f6 --- /dev/null +++ b/apps/build-docs/scripts/build.ts @@ -0,0 +1,23 @@ +import BuildHelper from "../../../scripts/build-utils"; + +const build = new BuildHelper("apps/build-docs"); + +async function main() { + // Build the CLI and other TypeScript files + await build.buildBackend([ + "src/cli.ts", + "src/main.ts", + "src/build-docs.ts", + "src/swagger.ts", + "src/script-api.ts", + "src/context.ts" + ]); + + // Copy HTML template + build.copy("src/index.html", "index.html"); + + // Copy node modules dependencies if needed + build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]); +} + +main(); diff --git a/apps/build-docs/src/backend_script_entrypoint.ts b/apps/build-docs/src/backend_script_entrypoint.ts index bc9087c0c1..0447900b6b 100644 --- a/apps/build-docs/src/backend_script_entrypoint.ts +++ b/apps/build-docs/src/backend_script_entrypoint.ts @@ -13,8 +13,12 @@ * Make sure to keep in line with backend's `script_context.ts`. */ -export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js"; -export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js"; +export type { + default as AbstractBeccaEntity +} from "../../server/src/becca/entities/abstract_becca_entity.js"; +export type { + default as BAttachment +} from "../../server/src/becca/entities/battachment.js"; export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js"; export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js"; export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js"; @@ -31,6 +35,7 @@ export type { Api }; const fakeNote = new BNote(); /** - * The `api` global variable allows access to the backend script API, which is documented in {@link Api}. + * The `api` global variable allows access to the backend script API, + * which is documented in {@link Api}. */ export const api: Api = new BackendScriptApi(fakeNote, {}); diff --git a/apps/build-docs/src/build-docs.ts b/apps/build-docs/src/build-docs.ts index 5d1a0cdd6f..357ecb1d41 100644 --- a/apps/build-docs/src/build-docs.ts +++ b/apps/build-docs/src/build-docs.ts @@ -1,19 +1,90 @@ process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store"; -process.env.TRILIUM_RESOURCE_DIR = "../server/src"; +// Only set TRILIUM_RESOURCE_DIR if not already set (e.g., by Nix wrapper) +if (!process.env.TRILIUM_RESOURCE_DIR) { + process.env.TRILIUM_RESOURCE_DIR = "../server/src"; +} process.env.NODE_ENV = "development"; import cls from "@triliumnext/server/src/services/cls.js"; -import { dirname, join, resolve } from "path"; +import archiver from "archiver"; +import { execSync } from "child_process"; +import { WriteStream } from "fs"; import * as fs from "fs/promises"; import * as fsExtra from "fs-extra"; -import archiver from "archiver"; -import { WriteStream } from "fs"; -import { execSync } from "child_process"; +import yaml from "js-yaml"; +import { dirname, join, resolve } from "path"; + import BuildContext from "./context.js"; +interface NoteMapping { + rootNoteId: string; + path: string; + format: "markdown" | "html" | "share"; + ignoredFiles?: string[]; + exportOnly?: boolean; +} + +interface Config { + baseUrl: string; + noteMappings: NoteMapping[]; +} + const DOCS_ROOT = "../../../docs"; const OUTPUT_DIR = "../../site"; +// Load configuration from edit-docs-config.yaml +async function loadConfig(configPath?: string): Promise { + const pathsToTry = configPath + ? [resolve(configPath)] + : [ + join(process.cwd(), "edit-docs-config.yaml"), + join(__dirname, "../../../edit-docs-config.yaml") + ]; + + for (const path of pathsToTry) { + try { + const configContent = await fs.readFile(path, "utf-8"); + const config = yaml.load(configContent) as Config; + + // Resolve all paths relative to the config file's directory + const CONFIG_DIR = dirname(path); + config.noteMappings = config.noteMappings.map((mapping) => ({ + ...mapping, + path: resolve(CONFIG_DIR, mapping.path) + })); + + return config; + } catch (error) { + if (error.code !== "ENOENT") { + throw error; // rethrow unexpected errors + } + } + } + + return null; // No config file found +} + +async function exportDocs( + noteId: string, + format: "markdown" | "html" | "share", + outputPath: string, + ignoredFiles?: string[] +) { + const zipFilePath = `output-${noteId}.zip`; + try { + const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js")) + .default; + await exportToZipFile(noteId, format, zipFilePath, {}); + + const ignoredSet = ignoredFiles ? new Set(ignoredFiles) : undefined; + await extractZip(zipFilePath, outputPath, ignoredSet); + } finally { + if (await fsExtra.exists(zipFilePath)) { + await fsExtra.rm(zipFilePath); + } + } +} + async function importAndExportDocs(sourcePath: string, outputSubDir: string) { const note = await importData(sourcePath); @@ -21,15 +92,18 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) { const zipName = outputSubDir || "user-guide"; const zipFilePath = `output-${zipName}.zip`; try { - const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default; + const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")) + .default; const branch = note.getParentBranches()[0]; - const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default( - "no-progress-reporting", - "export", - null - ); + const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")) + .default( + "no-progress-reporting", + "export", + null + ); const fileOutputStream = fsExtra.createWriteStream(zipFilePath); await exportToZip(taskContext, branch, "share", fileOutputStream); + const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js"); await waitForStreamToFinish(fileOutputStream); // Output to root directory if outputSubDir is empty, otherwise to subdirectory @@ -42,7 +116,7 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) { } } -async function buildDocsInner() { +async function buildDocsInner(config?: Config) { const i18n = await import("@triliumnext/server/src/services/i18n.js"); await i18n.initializeTranslations(); @@ -53,18 +127,49 @@ async function buildDocsInner() { const beccaLoader = await import("../../server/src/becca/becca_loader.js"); await beccaLoader.beccaLoaded; - // Build User Guide - console.log("Building User Guide..."); - await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide"); + if (config) { + // Config-based build (reads from edit-docs-config.yaml) + console.log("Building documentation from config file..."); - // Build Developer Guide - console.log("Building Developer Guide..."); - await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide"); + // Import all non-export-only mappings + for (const mapping of config.noteMappings) { + if (!mapping.exportOnly) { + console.log(`Importing from ${mapping.path}...`); + await importData(mapping.path); + } + } - // Copy favicon. - await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico")); - await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico")); - await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico")); + // Export all mappings + for (const mapping of config.noteMappings) { + if (mapping.exportOnly) { + console.log(`Exporting ${mapping.format} to ${mapping.path}...`); + await exportDocs( + mapping.rootNoteId, + mapping.format, + mapping.path, + mapping.ignoredFiles + ); + } + } + } else { + // Legacy hardcoded build (for backward compatibility) + console.log("Building User Guide..."); + await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide"); + + console.log("Building Developer Guide..."); + await importAndExportDocs( + join(__dirname, DOCS_ROOT, "Developer Guide"), + "developer-guide" + ); + + // Copy favicon. + await fs.copyFile("../../apps/website/src/assets/favicon.ico", + join(OUTPUT_DIR, "favicon.ico")); + await fs.copyFile("../../apps/website/src/assets/favicon.ico", + join(OUTPUT_DIR, "user-guide", "favicon.ico")); + await fs.copyFile("../../apps/website/src/assets/favicon.ico", + join(OUTPUT_DIR, "developer-guide", "favicon.ico")); + } console.log("Documentation built successfully!"); } @@ -91,12 +196,13 @@ async function createImportZip(path: string) { zlib: { level: 0 } }); - console.log("Archive path is ", resolve(path)) + console.log("Archive path is ", resolve(path)); archive.directory(path, "/"); const outputStream = fsExtra.createWriteStream(inputFile); archive.pipe(outputStream); archive.finalize(); + const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js"); await waitForStreamToFinish(outputStream); try { @@ -106,15 +212,15 @@ async function createImportZip(path: string) { } } -function waitForStreamToFinish(stream: WriteStream) { - return new Promise((res, rej) => { - stream.on("finish", () => res()); - stream.on("error", (err) => rej(err)); - }); -} -export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set) { - const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js")); +export async function extractZip( + zipFilePath: string, + outputPath: string, + ignoredFiles?: Set +) { + const { readZipFile, readContent } = (await import( + "@triliumnext/server/src/services/import/zip.js" + )); await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => { // We ignore directories since they can appear out of order anyway. if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) { @@ -129,6 +235,27 @@ export async function extractZip(zipFilePath: string, outputPath: string, ignore }); } +export async function buildDocsFromConfig(configPath?: string, gitRootDir?: string) { + const config = await loadConfig(configPath); + + if (gitRootDir) { + // Build the share theme if we have a gitRootDir (for Trilium project) + execSync(`pnpm run --filter share-theme build`, { + stdio: "inherit", + cwd: gitRootDir + }); + } + + // Trigger the actual build. + await new Promise((res, rej) => { + cls.init(() => { + buildDocsInner(config ?? undefined) + .catch(rej) + .then(res); + }); + }); +} + export default async function buildDocs({ gitRootDir }: BuildContext) { // Build the share theme. execSync(`pnpm run --filter share-theme build`, { diff --git a/apps/build-docs/src/cli.ts b/apps/build-docs/src/cli.ts new file mode 100644 index 0000000000..8aee089701 --- /dev/null +++ b/apps/build-docs/src/cli.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env node + +import packageJson from "../package.json" with { type: "json" }; +import { buildDocsFromConfig } from "./build-docs.js"; + +// Parse command-line arguments +function parseArgs() { + const args = process.argv.slice(2); + let configPath: string | undefined; + let showHelp = false; + let showVersion = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--config" || args[i] === "-c") { + configPath = args[i + 1]; + if (!configPath) { + console.error("Error: --config/-c requires a path argument"); + process.exit(1); + } + i++; // Skip the next argument as it's the value + } else if (args[i] === "--help" || args[i] === "-h") { + showHelp = true; + } else if (args[i] === "--version" || args[i] === "-v") { + showVersion = true; + } + } + + return { configPath, showHelp, showVersion }; +} + +function getVersion(): string { + return packageJson.version; +} + +function printHelp() { + const version = getVersion(); + console.log(` +Usage: trilium-build-docs [options] + +Options: + -c, --config Path to the configuration file + (default: edit-docs-config.yaml in current directory) + -h, --help Display this help message + -v, --version Display version information + +Description: + Builds documentation from Trilium note structure and exports to various formats. + Configuration file should be in YAML format with the following structure: + + baseUrl: "https://example.com" + noteMappings: + - rootNoteId: "noteId123" + path: "docs" + format: "markdown" + - rootNoteId: "noteId456" + path: "public/docs" + format: "share" + exportOnly: true + +Version: ${version} +`); +} + +function printVersion() { + const version = getVersion(); + console.log(version); +} + +async function main() { + const { configPath, showHelp, showVersion } = parseArgs(); + + if (showHelp) { + printHelp(); + process.exit(0); + } else if (showVersion) { + printVersion(); + process.exit(0); + } + + try { + await buildDocsFromConfig(configPath); + process.exit(0); + } catch (error) { + console.error("Error building documentation:", error); + process.exit(1); + } +} + +main(); diff --git a/apps/build-docs/src/frontend_script_entrypoint.ts b/apps/build-docs/src/frontend_script_entrypoint.ts index 768774eca6..7aba11b2a4 100644 --- a/apps/build-docs/src/frontend_script_entrypoint.ts +++ b/apps/build-docs/src/frontend_script_entrypoint.ts @@ -13,16 +13,19 @@ * Make sure to keep in line with frontend's `script_context.ts`. */ -export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js"; export type { default as FAttachment } from "../../client/src/entities/fattachment.js"; export type { default as FAttribute } from "../../client/src/entities/fattribute.js"; export type { default as FBranch } from "../../client/src/entities/fbranch.js"; export type { default as FNote } from "../../client/src/entities/fnote.js"; export type { Api } from "../../client/src/services/frontend_script_api.js"; -export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js"; +export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js"; +export type { + default as NoteContextAwareWidget +} from "../../client/src/widgets/note_context_aware_widget.js"; export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js"; import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js"; -//@ts-expect-error + +// @ts-expect-error - FrontendScriptApi is not directly exportable as Api without this simulation. export const api: Api = new FrontendScriptApi(); diff --git a/apps/build-docs/src/main.ts b/apps/build-docs/src/main.ts index d94ada167b..cca17125d8 100644 --- a/apps/build-docs/src/main.ts +++ b/apps/build-docs/src/main.ts @@ -1,9 +1,10 @@ -import { join } from "path"; -import BuildContext from "./context"; -import buildSwagger from "./swagger"; import { cpSync, existsSync, mkdirSync, rmSync } from "fs"; +import { join } from "path"; + import buildDocs from "./build-docs"; +import BuildContext from "./context"; import buildScriptApi from "./script-api"; +import buildSwagger from "./swagger"; const context: BuildContext = { gitRootDir: join(__dirname, "../../../"), diff --git a/apps/build-docs/src/script-api.ts b/apps/build-docs/src/script-api.ts index 8473ae3a02..2c62477474 100644 --- a/apps/build-docs/src/script-api.ts +++ b/apps/build-docs/src/script-api.ts @@ -1,7 +1,8 @@ import { execSync } from "child_process"; -import BuildContext from "./context"; import { join } from "path"; +import BuildContext from "./context"; + export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) { // Generate types execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir }); diff --git a/apps/build-docs/src/swagger.ts b/apps/build-docs/src/swagger.ts index b3677aeebe..2af458ec47 100644 --- a/apps/build-docs/src/swagger.ts +++ b/apps/build-docs/src/swagger.ts @@ -1,7 +1,8 @@ -import BuildContext from "./context"; -import { join } from "path"; import { execSync } from "child_process"; import { mkdirSync } from "fs"; +import { join } from "path"; + +import BuildContext from "./context"; interface BuildInfo { specPath: string; @@ -27,6 +28,9 @@ export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) { const absSpecPath = join(gitRootDir, specPath); const targetDir = join(baseDir, outDir); mkdirSync(targetDir, { recursive: true }); - execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" }); + execSync( + `pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, + { stdio: "inherit" } + ); } } diff --git a/apps/build-docs/tsconfig.json b/apps/build-docs/tsconfig.json index 858921cfb6..99c9b71b37 100644 --- a/apps/build-docs/tsconfig.json +++ b/apps/build-docs/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.base.json", - "include": [], + "include": [ + "scripts/**/*.ts" + ], "references": [ { "path": "../server" diff --git a/apps/build-docs/typedoc.backend.json b/apps/build-docs/typedoc.backend.json index 1781774c6f..a7f91c42e4 100644 --- a/apps/build-docs/typedoc.backend.json +++ b/apps/build-docs/typedoc.backend.json @@ -4,6 +4,7 @@ "entryPoints": [ "src/backend_script_entrypoint.ts" ], + "tsconfig": "tsconfig.app.json", "plugin": [ "typedoc-plugin-missing-exports" ] diff --git a/apps/build-docs/typedoc.frontend.json b/apps/build-docs/typedoc.frontend.json index f07d20dc71..c462004712 100644 --- a/apps/build-docs/typedoc.frontend.json +++ b/apps/build-docs/typedoc.frontend.json @@ -4,6 +4,7 @@ "entryPoints": [ "src/frontend_script_entrypoint.ts" ], + "tsconfig": "tsconfig.app.json", "plugin": [ "typedoc-plugin-missing-exports" ] diff --git a/apps/client/package.json b/apps/client/package.json index 09c9b51052..eb5f39aca1 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -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.7.1", + "@preact/signals": "2.8.1", "@triliumnext/ckeditor5": "workspace:*", "@triliumnext/codemirror": "workspace:*", "@triliumnext/commons": "workspace:*", @@ -44,7 +44,7 @@ "draggabilly": "3.0.0", "force-graph": "1.51.1", "globals": "17.3.0", - "i18next": "25.8.6", + "i18next": "25.8.13", "i18next-http-backend": "3.0.2", "jquery": "4.0.0", "jquery.fancytree": "2.38.5", @@ -54,14 +54,14 @@ "leaflet": "1.9.4", "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", - "marked": "17.0.2", - "mermaid": "11.12.2", - "mind-elixir": "5.8.0", + "marked": "17.0.3", + "mermaid": "11.12.3", + "mind-elixir": "5.8.3", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "preact": "10.28.3", + "preact": "10.28.4", "react-i18next": "16.5.4", - "react-window": "2.2.6", + "react-window": "2.2.7", "reveal.js": "5.2.1", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", @@ -69,7 +69,7 @@ }, "devDependencies": { "@ckeditor/ckeditor5-inspector": "5.0.0", - "@prefresh/vite": "2.4.11", + "@prefresh/vite": "2.4.12", "@types/bootstrap": "5.2.10", "@types/jquery": "3.5.33", "@types/leaflet": "1.9.21", @@ -78,7 +78,7 @@ "@types/reveal.js": "5.2.2", "@types/tabulator-tables": "6.3.1", "copy-webpack-plugin": "13.0.1", - "happy-dom": "20.6.1", + "happy-dom": "20.7.0", "lightningcss": "1.31.1", "script-loader": "0.7.2", "vite-plugin-static-copy": "3.2.0" diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index f161d7adb1..2b515f2412 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -700,6 +700,15 @@ export default class FNote { return this.hasAttribute(LABEL, name); } + /** + * Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import). + * @param name the name of the label to look for. + * @returns `true` if the label exists, or its version with the `disabled:` prefix. + */ + hasLabelOrDisabled(name: string) { + return this.hasLabel(name) || this.hasLabel(`disabled:${name}`); + } + /** * @param name - label name * @returns true if label exists (including inherited) and does not have "false" value. diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 8b162bced3..3fc5422b0a 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -23,10 +23,7 @@ import NoteTreeWidget from "../widgets/note_tree.js"; import NoteWrapperWidget from "../widgets/note_wrapper.js"; import NoteDetail from "../widgets/NoteDetail.jsx"; import QuickSearchWidget from "../widgets/quick_search.js"; -import { useNoteContext } from "../widgets/react/hooks.jsx"; -import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; -import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; -import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx"; +import ScrollPadding from "../widgets/scroll_padding"; import SearchResult from "../widgets/search_result.jsx"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import { applyModals } from "./layout_commons.js"; @@ -78,7 +75,7 @@ export default class MobileLayout { .child() .child() .child() - .child() + .child() ) .child() .child(new FindWidget()) @@ -102,13 +99,3 @@ export default class MobileLayout { return rootContainer; } } - -function FilePropertiesWrapper() { - const { note, ntxId } = useNoteContext(); - - return ( -
- {note?.type === "file" && } -
- ); -} diff --git a/apps/client/src/services/attributes.ts b/apps/client/src/services/attributes.ts index 2bff933244..77558c9464 100644 --- a/apps/client/src/services/attributes.ts +++ b/apps/client/src/services/attributes.ts @@ -168,6 +168,49 @@ function isAffecting(attrRow: AttributeRow, affectedNote: FNote | null | undefin return false; } +/** + * Toggles whether a dangerous attribute is enabled or not. When an attribute is disabled, its name is prefixed with `disabled:`. + * + * Note that this work for non-dangerous attributes as well. + * + * If there are multiple attributes with the same name, all of them will be toggled at the same time. + * + * @param note the note whose attribute to change. + * @param type the type of dangerous attribute (label or relation). + * @param name the name of the dangerous attribute. + * @param willEnable whether to enable or disable the attribute. + * @returns a promise that will resolve when the request to the server completes. + */ +async function toggleDangerousAttribute(note: FNote, type: "label" | "relation", name: string, willEnable: boolean) { + const attrs = [ + ...note.getOwnedAttributes(type, name), + ...note.getOwnedAttributes(type, `disabled:${name}`) + ]; + + for (const attr of attrs) { + const baseName = getNameWithoutDangerousPrefix(attr.name); + const newName = willEnable ? baseName : `disabled:${baseName}`; + if (newName === attr.name) continue; + + // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically. + if (attr.type === "label") { + await setLabel(note.noteId, newName, attr.value); + } else { + await setRelation(note.noteId, newName, attr.value); + } + await removeAttributeById(note.noteId, attr.attributeId); + } +} + +/** + * Returns the name of an attribute without the `disabled:` prefix, or the same name if it's not disabled. + * @param name the name of an attribute. + * @returns the name without the `disabled:` prefix. + */ +function getNameWithoutDangerousPrefix(name: string) { + return name.startsWith("disabled:") ? name.substring(9) : name; +} + export default { addLabel, setLabel, @@ -177,5 +220,7 @@ export default { removeAttributeById, removeOwnedLabelByName, removeOwnedRelationByName, - isAffecting + isAffecting, + toggleDangerousAttribute, + getNameWithoutDangerousPrefix }; diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index d33ba76a0a..7cee01812b 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -2,7 +2,6 @@ import { h, VNode } from "preact"; import BasicWidget, { ReactWrappedWidget } from "../widgets/basic_widget.js"; import RightPanelWidget from "../widgets/right_panel_widget.js"; -import froca from "./froca.js"; import type { Entity } from "./frontend_script_api.js"; import { WidgetDefinitionWithType } from "./frontend_script_api_preact.js"; import { t } from "./i18n.js"; @@ -38,15 +37,18 @@ async function getAndExecuteBundle(noteId: string, originEntity = null, script = export type ParentName = "left-pane" | "center-pane" | "note-detail-pane" | "right-pane"; -export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery) { +export async function executeBundleWithoutErrorHandling(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery) { const apiContext = await ScriptContext(bundle.noteId, bundle.allNoteIds, originEntity, $container); + return await function () { + return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); + }.call(apiContext); +} +export async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery) { try { - return await function () { - return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); - }.call(apiContext); - } catch (e: any) { - showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: e.message })); + return await executeBundleWithoutErrorHandling(bundle, originEntity, $container); + } catch (e: unknown) { + showErrorForScriptNote(bundle.noteId, t("toast.bundle-error.message", { message: getErrorMessage(e) })); logError("Widget initialization failed: ", e); } } diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index aca5d3efe3..148d59acd0 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -15,7 +15,7 @@ import protectedSessionService from "./protected_session.js"; import protectedSessionHolder from "./protected_session_holder.js"; import renderService from "./render.js"; import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js"; -import utils from "./utils.js"; +import utils, { getErrorMessage } from "./utils.js"; let idCounter = 1; @@ -62,7 +62,10 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo } else if (type === "render" && entity instanceof FNote) { const $content = $("
"); - await renderService.render(entity, $content); + await renderService.render(entity, $content, (e) => { + const $error = $("
").addClass("admonition caution").text(typeof e === "string" ? e : getErrorMessage(e)); + $content.empty().append($error); + }); $renderedContent.append($content); } else if (type === "doc" && "noteId" in entity) { diff --git a/apps/client/src/services/date_notes.ts b/apps/client/src/services/date_notes.ts index 340ebf7f8c..21709b1bb0 100644 --- a/apps/client/src/services/date_notes.ts +++ b/apps/client/src/services/date_notes.ts @@ -1,4 +1,5 @@ import { dayjs } from "@triliumnext/commons"; + import type { FNoteRow } from "../entities/fnote.js"; import froca from "./froca.js"; import server from "./server.js"; @@ -14,8 +15,13 @@ async function getTodayNote() { return await getDayNote(dayjs().format("YYYY-MM-DD")); } -async function getDayNote(date: string) { - const note = await server.get(`special-notes/days/${date}`, "date-note"); +async function getDayNote(date: string, calendarRootId?: string) { + let url = `special-notes/days/${date}`; + if (calendarRootId) { + url += `?calendarRootId=${calendarRootId}`; + } + + const note = await server.get(url, "date-note"); await ws.waitForMaxKnownEntityChangeId(); diff --git a/apps/client/src/services/i18n.ts b/apps/client/src/services/i18n.ts index 5b5f38b762..c8bb9097d7 100644 --- a/apps/client/src/services/i18n.ts +++ b/apps/client/src/services/i18n.ts @@ -24,7 +24,8 @@ export async function initLocale() { backend: { loadPath: `${window.glob.assetPath}/translations/{{lng}}/{{ns}}.json` }, - returnEmptyString: false + returnEmptyString: false, + showSupportNotice: false }); await setDayjsLocale(locale); diff --git a/apps/client/src/services/note_tooltip.ts b/apps/client/src/services/note_tooltip.ts index 60af420468..23263966e2 100644 --- a/apps/client/src/services/note_tooltip.ts +++ b/apps/client/src/services/note_tooltip.ts @@ -1,20 +1,20 @@ -import treeService from "./tree.js"; -import linkService from "./link.js"; -import froca from "./froca.js"; -import utils from "./utils.js"; -import attributeRenderer from "./attribute_renderer.js"; -import contentRenderer from "./content_renderer.js"; import appContext from "../components/app_context.js"; import type FNote from "../entities/fnote.js"; +import attributeRenderer from "./attribute_renderer.js"; +import contentRenderer from "./content_renderer.js"; +import froca from "./froca.js"; import { t } from "./i18n.js"; +import linkService from "./link.js"; +import treeService from "./tree.js"; +import utils from "./utils.js"; // Track all elements that open tooltips let openTooltipElements: JQuery[] = []; let dismissTimer: ReturnType; function setupGlobalTooltip() { - $(document).on("mouseenter", "a:not(.no-tooltip-preview)", mouseEnterHandler); - $(document).on("mouseenter", "[data-href]:not(.no-tooltip-preview)", mouseEnterHandler); + $(document).on("pointerenter", "a:not(.no-tooltip-preview)", mouseEnterHandler); + $(document).on("pointerenter", "[data-href]:not(.no-tooltip-preview)", mouseEnterHandler); // close any note tooltip after click, this fixes the problem that sometimes tooltips remained on the screen $(document).on("click", (e) => { @@ -37,10 +37,12 @@ function dismissAllTooltips() { } function setupElementTooltip($el: JQuery) { - $el.on("mouseenter", mouseEnterHandler); + $el.on("pointerenter", mouseEnterHandler); } -async function mouseEnterHandler(this: HTMLElement) { +async function mouseEnterHandler(this: HTMLElement, e: JQuery.TriggeredEvent) { + if (e.pointerType !== "mouse") return; + const $link = $(this); if ($link.hasClass("no-tooltip-preview") || $link.hasClass("disabled")) { @@ -91,7 +93,7 @@ async function mouseEnterHandler(this: HTMLElement) { } const html = `
${content}
`; - const tooltipClass = "tooltip-" + Math.floor(Math.random() * 999_999_999); + const tooltipClass = `tooltip-${ Math.floor(Math.random() * 999_999_999)}`; // we need to check if we're still hovering over the element // since the operation to get tooltip content was async, it is possible that @@ -224,7 +226,7 @@ function renderFootnoteOrAnchor($link: JQuery, url: string) { } let footnoteContent = $targetContent.html(); - footnoteContent = `
${footnoteContent}
` + footnoteContent = `
${footnoteContent}
`; return footnoteContent || ""; } diff --git a/apps/client/src/services/render.ts b/apps/client/src/services/render.ts deleted file mode 100644 index adfd8a4949..0000000000 --- a/apps/client/src/services/render.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { h, VNode } from "preact"; - -import type FNote from "../entities/fnote.js"; -import { renderReactWidgetAtElement } from "../widgets/react/react_utils.jsx"; -import bundleService, { type Bundle } from "./bundle.js"; -import froca from "./froca.js"; -import server from "./server.js"; - -async function render(note: FNote, $el: JQuery) { - const relations = note.getRelations("renderNote"); - const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId); - - $el.empty().toggle(renderNoteIds.length > 0); - - for (const renderNoteId of renderNoteIds) { - const bundle = await server.post(`script/bundle/${renderNoteId}`); - - const $scriptContainer = $("
"); - $el.append($scriptContainer); - - $scriptContainer.append(bundle.html); - - // async so that scripts cannot block trilium execution - bundleService.executeBundle(bundle, note, $scriptContainer).then(result => { - // Render JSX - if (bundle.html === "") { - renderIfJsx(bundle, result, $el); - } - }); - } - - return renderNoteIds.length > 0; -} - -async function renderIfJsx(bundle: Bundle, result: unknown, $el: JQuery) { - // Ensure the root script note is actually a JSX. - const rootScriptNoteId = await froca.getNote(bundle.noteId); - if (rootScriptNoteId?.mime !== "text/jsx") return; - - // Ensure the output is a valid el. - if (typeof result !== "function") return; - - // Obtain the parent component. - const closestComponent = glob.getComponentByEl($el.closest(".component")[0]); - if (!closestComponent) return; - - // Render the element. - const el = h(result as () => VNode, {}); - renderReactWidgetAtElement(closestComponent, el, $el[0]); -} - -export default { - render -}; diff --git a/apps/client/src/services/render.tsx b/apps/client/src/services/render.tsx new file mode 100644 index 0000000000..682efa8871 --- /dev/null +++ b/apps/client/src/services/render.tsx @@ -0,0 +1,86 @@ +import { Component, h, VNode } from "preact"; + +import type FNote from "../entities/fnote.js"; +import { renderReactWidgetAtElement } from "../widgets/react/react_utils.jsx"; +import { type Bundle, executeBundleWithoutErrorHandling } from "./bundle.js"; +import froca from "./froca.js"; +import server from "./server.js"; + +type ErrorHandler = (e: unknown) => void; + +async function render(note: FNote, $el: JQuery, onError?: ErrorHandler) { + const relations = note.getRelations("renderNote"); + const renderNoteIds = relations.map((rel) => rel.value).filter((noteId) => noteId); + + $el.empty().toggle(renderNoteIds.length > 0); + + try { + for (const renderNoteId of renderNoteIds) { + const bundle = await server.postWithSilentInternalServerError(`script/bundle/${renderNoteId}`); + + const $scriptContainer = $("
"); + $el.append($scriptContainer); + + $scriptContainer.append(bundle.html); + + // async so that scripts cannot block trilium execution + executeBundleWithoutErrorHandling(bundle, note, $scriptContainer) + .catch(onError) + .then(result => { + // Render JSX + if (bundle.html === "") { + renderIfJsx(bundle, result, $el, onError).catch(onError); + } + }); + } + + return renderNoteIds.length > 0; + } catch (e) { + if (typeof e === "string" && e.startsWith("{") && e.endsWith("}")) { + try { + onError?.(JSON.parse(e)); + } catch (e) { + onError?.(e); + } + } else { + onError?.(e); + } + } +} + +async function renderIfJsx(bundle: Bundle, result: unknown, $el: JQuery, onError?: ErrorHandler) { + // Ensure the root script note is actually a JSX. + const rootScriptNoteId = await froca.getNote(bundle.noteId); + if (rootScriptNoteId?.mime !== "text/jsx") return; + + // Ensure the output is a valid el. + if (typeof result !== "function") return; + + // Obtain the parent component. + const closestComponent = glob.getComponentByEl($el.closest(".component")[0]); + if (!closestComponent) return; + + // Render the element. + const UserErrorBoundary = class UserErrorBoundary extends Component { + constructor(props: object) { + super(props); + this.state = { error: null }; + } + + componentDidCatch(error: unknown) { + onError?.(error); + this.setState({ error }); + } + + render() { + if ("error" in this.state && this.state?.error) return null; + return this.props.children; + } + }; + const el = h(UserErrorBoundary, {}, h(result as () => VNode, {})); + renderReactWidgetAtElement(closestComponent, el, $el[0]); +} + +export default { + render +}; diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index 381c58a3cf..fb1e598ec2 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -73,6 +73,10 @@ async function post(url: string, data?: unknown, componentId?: string) { return await call("POST", url, componentId, { data }); } +async function postWithSilentInternalServerError(url: string, data?: unknown, componentId?: string) { + return await call("POST", url, componentId, { data, silentInternalServerError: true }); +} + async function put(url: string, data?: unknown, componentId?: string) { return await call("PUT", url, componentId, { data }); } @@ -111,6 +115,7 @@ let maxKnownEntityChangeId = 0; interface CallOptions { data?: unknown; silentNotFound?: boolean; + silentInternalServerError?: boolean; // If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc. raw?: boolean; } @@ -143,7 +148,7 @@ async function call(method: string, url: string, componentId?: string, option }); })) as any; } else { - resp = await ajax(url, method, data, headers, !!options.silentNotFound, options.raw); + resp = await ajax(url, method, data, headers, options); } const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"]; @@ -155,10 +160,7 @@ async function call(method: string, url: string, componentId?: string, option return resp.body as T; } -/** - * @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc. - */ -function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean, raw?: boolean): Promise { +function ajax(url: string, method: string, data: unknown, headers: Headers, opts: CallOptions): Promise { return new Promise((res, rej) => { const options: JQueryAjaxSettings = { url: window.glob.baseApiUrl + url, @@ -190,7 +192,9 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile // don't report requests that are rejected by the browser, usually when the user is refreshing or going to a different page. rej("rejected by browser"); return; - } else if (silentNotFound && jqXhr.status === 404) { + } else if (opts.silentNotFound && jqXhr.status === 404) { + // report nothing + } else if (opts.silentInternalServerError && jqXhr.status === 500) { // report nothing } else { await reportError(method, url, jqXhr.status, jqXhr.responseText); @@ -200,7 +204,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile } }; - if (raw) { + if (opts.raw) { options.dataType = "text"; } @@ -299,6 +303,7 @@ export default { get, getWithSilentNotFound, post, + postWithSilentInternalServerError, put, patch, remove, diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 0fc2a962e1..8950f3fd3f 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -153,6 +153,11 @@ textarea, background: var(--input-background-color); } +.form-control:disabled { + background-color: var(--input-background-color); + opacity: 0.6; +} + .form-control:focus { color: var(--input-text-color); background: var(--input-background-color); @@ -942,6 +947,7 @@ table.promoted-attributes-in-tooltip th { color: var(--muted-text-color); opacity: 0.6; line-height: 1; + word-wrap: break-word; } .aa-dropdown-menu .aa-suggestion p { @@ -1581,7 +1587,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { position: absolute; top: 0; inset-inline-start: 0; - bottom: 0; + height: 100dvh; width: 85vw; padding-top: env(safe-area-inset-top); transition: transform 250ms ease-in-out; @@ -1645,13 +1651,27 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { word-break: break-all; } - body.mobile .jump-to-note-dialog .modal-content { - overflow-y: auto; - } + body.mobile .jump-to-note-dialog { + .modal-header { + padding-bottom: 0.75rem !important; + } - body.mobile .jump-to-note-dialog .modal-dialog .aa-dropdown-menu { - max-height: unset; - overflow: auto; + .modal-content { + padding-bottom: 0 !important; + } + + .modal-body { + overflow-y: auto; + } + + .aa-dropdown-menu { + max-height: unset; + overflow: auto; + } + + .aa-suggestion { + padding-inline: 0; + } } body.mobile .modal-dialog .dropdown-menu { diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index f8fb305726..dfdc209040 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -210,6 +210,7 @@ --badge-share-background-color: #4d4d4d; --badge-clipped-note-background-color: #295773; --badge-execute-background-color: #604180; + --badge-active-content-background-color: rgb(12, 68, 70); --note-icon-background-color: #444444; --note-icon-color: #d4d4d4; @@ -238,9 +239,9 @@ --bottom-panel-background-color: #11111180; --bottom-panel-title-bar-background-color: #3F3F3F80; - + --status-bar-border-color: var(--main-border-color); - + --scrollbar-thumb-color: #fdfdfd5c; --scrollbar-thumb-hover-color: #ffffff7d; --scrollbar-background-color: transparent; @@ -290,6 +291,15 @@ --ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75); --ck-editor-toolbar-dropdown-button-open-background: #ffffff14; + --note-list-view-icon-color: var(--left-pane-icon-color); + --note-list-view-large-icon-background: var(--note-icon-background-color); + --note-list-view-large-icon-color: var(--note-icon-color); + --note-list-view-search-result-highlight-background: transparent; + --note-list-view-search-result-highlight-color: var(--quick-search-result-highlight-color); + --note-list-view-content-background: rgba(0, 0, 0, .2); + --note-list-view-content-search-result-highlight-background: var(--quick-search-result-highlight-color); + --note-list-view-content-search-result-highlight-color: black; + --calendar-coll-event-background-saturation: 25%; --calendar-coll-event-background-lightness: 20%; --calendar-coll-event-background-color: #3c3c3c; @@ -303,7 +313,8 @@ * Dark color scheme tweaks */ -#left-pane .fancytree-node.tinted { +#left-pane .fancytree-node.tinted, +.nested-note-list-item.use-note-color { --custom-color: var(--dark-theme-custom-color); /* The background color of the active item in the note tree. @@ -337,12 +348,24 @@ body .todo-list input[type="checkbox"]:not(:checked):before { --promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%); } +.modal.tab-bar-modal .tabs .tab-card.with-hue { + background-color: hsl(var(--bg-hue), 8.8%, 11.2%); + border-color: hsl(var(--bg-hue), 9.4%, 25.1%); +} + +.modal.tab-bar-modal .tabs .tab-card.active.with-hue { + background-color: hsl(var(--bg-hue), 8.8%, 16.2%); + border-color: hsl(var(--bg-hue), 9.4%, 25.1%); +} + + .use-note-color { --custom-color: var(--dark-theme-custom-color); } .note-split.with-hue, -.quick-edit-dialog-wrapper.with-hue { +.quick-edit-dialog-wrapper.with-hue, +.nested-note-list-item.with-hue { --note-icon-custom-background-color: hsl(var(--custom-color-hue), 15.8%, 30.9%); --note-icon-custom-color: hsl(var(--custom-color-hue), 100%, 76.5%); --note-icon-hover-custom-background-color: hsl(var(--custom-color-hue), 28.3%, 36.7%); @@ -351,4 +374,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before { .note-split.with-hue *::selection, .quick-edit-dialog-wrapper.with-hue *::selection { --selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%); -} \ No newline at end of file +} diff --git a/apps/client/src/stylesheets/theme-next-light.css b/apps/client/src/stylesheets/theme-next-light.css index 2d7862ae00..4b6b37718c 100644 --- a/apps/client/src/stylesheets/theme-next-light.css +++ b/apps/client/src/stylesheets/theme-next-light.css @@ -202,6 +202,7 @@ --badge-share-background-color: #6b6b6b; --badge-clipped-note-background-color: #2284c0; --badge-execute-background-color: #7b47af; + --badge-active-content-background-color: rgb(27, 164, 168); --note-icon-background-color: #4f4f4f; --note-icon-color: white; @@ -288,6 +289,15 @@ --ck-editor-toolbar-button-on-shadow: none; --ck-editor-toolbar-dropdown-button-open-background: #0000000f; + --note-list-view-icon-color: var(--left-pane-icon-color); + --note-list-view-large-icon-background: var(--note-icon-background-color); + --note-list-view-large-icon-color: var(--note-icon-color); + --note-list-view-search-result-highlight-background: transparent; + --note-list-view-search-result-highlight-color: var(--quick-search-result-highlight-color); + --note-list-view-content-background: #b1b1b133; + --note-list-view-content-search-result-highlight-background: var(--quick-search-result-highlight-color); + --note-list-view-content-search-result-highlight-color: white; + --calendar-coll-event-background-lightness: 95%; --calendar-coll-event-background-saturation: 80%; --calendar-coll-event-background-color: #eaeaea; @@ -297,7 +307,8 @@ --calendar-coll-today-background-color: #00000006; } -#left-pane .fancytree-node.tinted { +#left-pane .fancytree-node.tinted, +.nested-note-list-item.use-note-color { --custom-color: var(--light-theme-custom-color); /* The background color of the active item in the note tree. @@ -312,8 +323,19 @@ --promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%); } +.modal.tab-bar-modal .tabs .tab-card.with-hue { + background-color: hsl(var(--bg-hue), 56%, 96%); + border-color: hsl(var(--bg-hue), 33%, 41%); +} + +.modal.tab-bar-modal .tabs .tab-card.active.with-hue { + background-color: hsl(var(--bg-hue), 86%, 96%); + border-color: hsl(var(--bg-hue), 33%, 41%); +} + .note-split.with-hue, -.quick-edit-dialog-wrapper.with-hue { +.quick-edit-dialog-wrapper.with-hue, +.nested-note-list-item.with-hue { --note-icon-custom-background-color: hsl(var(--custom-color-hue), 44.5%, 43.1%); --note-icon-custom-color: hsl(var(--custom-color-hue), 91.3%, 91%); --note-icon-hover-custom-background-color: hsl(var(--custom-color-hue), 55.1%, 50.2%); @@ -322,4 +344,4 @@ .note-split.with-hue *::selection, .quick-edit-dialog-wrapper.with-hue *::selection { --selection-background-color: hsl(var(--custom-color-hue), 60%, 90%); -} \ No newline at end of file +} diff --git a/apps/client/src/stylesheets/theme-next/forms.css b/apps/client/src/stylesheets/theme-next/forms.css index bccf7ab6d4..cbe5f2cda0 100644 --- a/apps/client/src/stylesheets/theme-next/forms.css +++ b/apps/client/src/stylesheets/theme-next/forms.css @@ -145,6 +145,10 @@ button.tn-low-profile:hover { font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio)); } +:root .icon-action.disabled::before { + opacity: .5; +} + :root .icon-action:not(.global-menu-button):hover, :root .icon-action:not(.global-menu-button).show, :root .tn-tool-button:hover, @@ -838,7 +842,7 @@ input[type="range"] { text-align: center; } -.tn-centered-form input, +.tn-centered-form .input-group, .tn-centered-form button { margin-top: 12px; -} \ No newline at end of file +} diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 0df9d6686a..1ac2f13e77 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -751,12 +751,14 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i } } -#left-pane .fancytree-expander { +#left-pane .fancytree-expander, +.nested-note-list-item .note-expander { opacity: 0.65; transition: opacity 150ms ease-in; } -#left-pane .fancytree-expander:hover { +#left-pane .fancytree-expander:hover, +.nested-note-list-item .note-expander:hover { opacity: 1; transition: opacity 300ms ease-out; } diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 1f748a99b3..58ac0f748a 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1008,7 +1008,7 @@ "no_attachments": "此笔记没有附件。" }, "book": { - "no_children_help": "此类型为书籍的笔记没有任何子笔记,因此没有内容显示。请参阅 wiki 了解详情。", + "no_children_help": "此集合没有任何子笔记,因此没有内容显示。", "drag_locked_title": "锁定编辑", "drag_locked_message": "无法拖拽,因为集合已被锁定编辑。" }, @@ -1064,10 +1064,6 @@ "default_new_note_title": "新笔记", "click_on_canvas_to_place_new_note": "点击画布以放置新笔记" }, - "render": { - "note_detail_render_help_1": "之所以显示此帮助说明,是因为这个类型为渲染 HTML 的笔记没有正常工作所需的关系。", - "note_detail_render_help_2": "渲染 HTML 笔记类型用于编写脚本。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript),然后这个笔记会把页面渲染出来。要使其正常工作,您需要定义一个名为 \"renderNote\" 的关系指向要渲染的 HTML 笔记。" - }, "backend_log": { "refresh": "刷新" }, @@ -2073,7 +2069,8 @@ "raster": "栅格", "vector_light": "矢量(浅色)", "vector_dark": "矢量(深色)", - "show-scale": "显示比例尺" + "show-scale": "显示比例尺", + "show-labels": "显示标记名称" }, "table_context_menu": { "delete_row": "删除行" @@ -2152,7 +2149,6 @@ "app-restart-required": "(需重启程序以应用更改)" }, "pagination": { - "page_title": "第 {{startIndex}} 页 - 第 {{endIndex}} 页", "total_notes": "{{count}} 篇笔记" }, "collections": { @@ -2272,6 +2268,43 @@ "url_placeholder": "输入或粘贴网站地址,例如 https://triliumnotes.org", "create_button": "创建网页视图", "invalid_url_title": "无效的地址", - "invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。" + "invalid_url_message": "请输入有效的网址,例如 https://triliumnotes.org。", + "disabled_description": "此网页视图来自外部来源。为保护您免受网络钓鱼或恶意内容侵害,该视图不会自动加载。若您信任该来源,可手动启用加载功能。", + "disabled_button_enable": "启用网页视图" + }, + "render": { + "setup_title": "在此笔记中显示自定义 HTML 或 Preact JSX", + "setup_create_sample_preact": "使用 Preact 建立范例笔记", + "setup_create_sample_html": "使用 HTML 建立范例笔记", + "setup_sample_created": "已建立一个范例笔记作为子笔记。", + "disabled_description": "此渲染笔记来自外部来源。为保护您免受恶意内容侵害,该功能默认处于禁用状态。启用前请确保您信任该来源。", + "disabled_button_enable": "启用渲染笔记" + }, + "active_content_badges": { + "type_icon_pack": "图标包", + "type_backend_script": "后端脚本", + "type_frontend_script": "前端脚本", + "type_widget": "小部件", + "type_app_css": "自定义 CSS", + "type_render_note": "渲染笔记", + "type_web_view": "网页视图", + "type_app_theme": "自定义主题", + "toggle_tooltip_enable_tooltip": "点击以启用此 {{type}}。", + "toggle_tooltip_disable_tooltip": "点击以禁用此 {{type}}。", + "menu_docs": "打开文档", + "menu_execute_now": "立即执行脚本", + "menu_run": "自动执行", + "menu_run_disabled": "手动", + "menu_run_backend_startup": "当后端启动时", + "menu_run_hourly": "每小时", + "menu_run_daily": "每日", + "menu_run_frontend_startup": "当桌面前端启动时", + "menu_run_mobile_startup": "当移动前端启动时", + "menu_change_to_widget": "更改为小部件", + "menu_change_to_frontend_script": "更改为前端脚本", + "menu_theme_base": "主题基底" + }, + "setup_form": { + "more_info": "了解更多" } } diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index 0cfcb85b57..9b8abcad8e 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -1007,7 +1007,7 @@ "no_attachments": "Diese Notiz enthält keine Anhänge." }, "book": { - "no_children_help": "Diese Notiz mit dem Notiztyp Buch besitzt keine Unternotizen, deshalb ist nichts zum Anzeigen vorhanden. Siehe Wiki für mehr Details.", + "no_children_help": "Diese Sammlung enthält keineUnternotizen, daher gibt es nichts anzuzeigen.", "drag_locked_title": "Für Bearbeitung gesperrt", "drag_locked_message": "Das Ziehen ist nicht möglich, da die Sammlung für die Bearbeitung gesperrt ist." }, @@ -1063,10 +1063,6 @@ "default_new_note_title": "neue Notiz", "click_on_canvas_to_place_new_note": "Klicke auf den Canvas, um eine neue Notiz zu platzieren" }, - "render": { - "note_detail_render_help_1": "Diese Hilfesnotiz wird angezeigt, da diese Notiz vom Typ „HTML rendern“ nicht über die erforderliche Beziehung verfügt, um ordnungsgemäß zu funktionieren.", - "note_detail_render_help_2": "Render-HTML-Notiztyp wird benutzt für scripting. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a Beziehung namens \"renderNote\" zeigend auf die HTML-Notiz zum rendern definieren." - }, "backend_log": { "refresh": "Aktualisieren" }, @@ -2090,7 +2086,8 @@ "raster": "Raster", "vector_light": "Vektor (Hell)", "vector_dark": "Vektor (Dunkel)", - "show-scale": "Zeige Skalierung" + "show-scale": "Zeige Skalierung", + "show-labels": "Zeige Markierungsnamen" }, "table_context_menu": { "delete_row": "Zeile entfernen" @@ -2157,7 +2154,6 @@ "percentage": "%" }, "pagination": { - "page_title": "Seite {{startIndex}} von {{endIndex}}", "total_notes": "{{count}} Notizen" }, "collections": { @@ -2287,6 +2283,43 @@ "url_placeholder": "Gib oder füge die Adresse der Webseite ein, zum Beispiel https://triliumnotes.org", "create_button": "Erstelle Web Ansicht", "invalid_url_title": "Ungültige Adresse", - "invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org." + "invalid_url_message": "Füge eine valide Webadresse ein, zum Beispiel https://triliumnotes.org.", + "disabled_description": "Diese Webansicht wurde von einer externen Quelle importiert. Um Sie vor Phishing oder schädlichen Inhalten zu schützen, wird sie nicht automatisch geladen. Sie können sie aktivieren, wenn Sie der Quelle vertrauen.", + "disabled_button_enable": "Webansicht aktivieren" + }, + "render": { + "setup_create_sample_html": "Eine Beispielnotiz mit HTML erstellen", + "setup_create_sample_preact": "Eine Beispielnotiz mit Preact erstellen", + "setup_title": "Benutzerdefiniertes HTML oder Preact JSX in dieser Notiz anzeigen", + "setup_sample_created": "Eine Beispielnotiz wurde als untergeordnete Notiz erstellt.", + "disabled_description": "Diese Rendering-Notizen stammen aus einer externen Quelle. Um Sie vor schädlichen Inhalten zu schützen, ist diese Funktion standardmäßig deaktiviert. Stellen Sie sicher, dass Sie der Quelle vertrauen, bevor Sie sie aktivieren.", + "disabled_button_enable": "Rendering-Notiz aktivieren" + }, + "active_content_badges": { + "type_icon_pack": "Icon-Paket", + "type_backend_script": "Backend-Skript", + "type_frontend_script": "Frontend-Skript", + "type_widget": "Widget", + "type_app_css": "Benutzerdefiniertes CSS", + "type_render_note": "Rendering-Notiz", + "type_web_view": "Webansicht", + "type_app_theme": "Benutzerdefiniertes Thema", + "toggle_tooltip_enable_tooltip": "Klicken, um diesen {{type}} zu aktivieren.", + "toggle_tooltip_disable_tooltip": "Klicken, um diesen {{type}} zu deaktivieren.", + "menu_docs": "Dokumentation öffnen", + "menu_execute_now": "Skript jetzt ausführen", + "menu_run": "Automatisch ausführen", + "menu_run_disabled": "Manuell", + "menu_run_backend_startup": "Wenn das Backend startet", + "menu_run_hourly": "Stündlich", + "menu_run_daily": "Täglich", + "menu_run_frontend_startup": "Wenn das Desktop-Frontend startet", + "menu_run_mobile_startup": "Wenn das mobile Frontend startet", + "menu_change_to_widget": "Zum Widget wechseln", + "menu_change_to_frontend_script": "Zum Frontend-Skript wechseln", + "menu_theme_base": "Themenbasis" + }, + "setup_form": { + "more_info": "Mehr erfahren" } } diff --git a/apps/client/src/translations/el/translation.json b/apps/client/src/translations/el/translation.json index 2155123a67..7512b9e705 100644 --- a/apps/client/src/translations/el/translation.json +++ b/apps/client/src/translations/el/translation.json @@ -53,6 +53,21 @@ "prefix": "Πρόθεμα: ", "save": "Αποθήκευση", "branch_prefix_saved": "Το πρόθεμα κλάδου αποθηκεύτηκε.", - "branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους." + "branch_prefix_saved_multiple": "Το πρόθεμα κλάδου αποθηκεύτηκε για {{count}} κλάδους.", + "affected_branches": "Επηρεαζόμενοι κλάδοι ({{count}}):" + }, + "bulk_actions": { + "bulk_actions": "Μαζικές ενέργειες", + "affected_notes": "Επηρεαζόμενες σημειώσεις", + "include_descendants": "Συμπερίληψη απογόνων των επιλεγμένων σημειώσεων", + "available_actions": "Διαθέσιμες ενέργειες", + "chosen_actions": "Επιλεγμένες ενέργειες", + "execute_bulk_actions": "Εκτέλεση μαζικών ενεργειών", + "bulk_actions_executed": "Οι μαζικές ενέργειες εκτελέστηκαν επιτυχώς.", + "none_yet": "Καμία ακόμη… προσθέστε μια ενέργεια επιλέγοντας μία από τις διαθέσιμες παραπάνω.", + "labels": "Ετικέτες", + "relations": "Συσχετίσεις", + "notes": "Σημειώσεις", + "other": "Λοιπά" } } diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index f434ffeddd..377a287183 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1010,7 +1010,7 @@ "no_attachments": "This note has no attachments." }, "book": { - "no_children_help": "This collection doesn't have any child notes so there's nothing to display. See wiki for details.", + "no_children_help": "This collection doesn't have any child notes so there's nothing to display.", "drag_locked_title": "Locked for editing", "drag_locked_message": "Dragging not allowed since the collection is locked for editing." }, @@ -1067,15 +1067,21 @@ "click_on_canvas_to_place_new_note": "Click on canvas to place new note" }, "render": { - "note_detail_render_help_1": "This help note is shown because this note of type Render HTML doesn't have required relation to function properly.", - "note_detail_render_help_2": "Render HTML note type is used for scripting. In short, you have a HTML code note (optionally with some JavaScript) and this note will render it. To make it work, you need to define a relation called \"renderNote\" pointing to the HTML note to render." + "setup_title": "Display custom HTML or Preact JSX inside this note", + "setup_create_sample_preact": "Create sample note with Preact", + "setup_create_sample_html": "Create sample note with HTML", + "setup_sample_created": "A sample note was created as a child note.", + "disabled_description": "This render notes comes from an external source. To protect you from malicious content, it is not enabled by default. Make sure you trust the source before enabling it.", + "disabled_button_enable": "Enable render note" }, "web_view_setup": { "title": "Create a live view of a webpage directly into Trilium", "url_placeholder": "Enter or paste the website address, for example https://triliumnotes.org", "create_button": "Create Web View", "invalid_url_title": "Invalid address", - "invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org." + "invalid_url_message": "Insert a valid web address, for example https://triliumnotes.org.", + "disabled_description": "This web view was imported from an external source. To help protect you from phishing or malicious content, it isn’t loading automatically. You can enable it if you trust the source.", + "disabled_button_enable": "Enable web view" }, "backend_log": { "refresh": "Refresh" @@ -2104,7 +2110,8 @@ "raster": "Raster", "vector_light": "Vector (Light)", "vector_dark": "Vector (Dark)", - "show-scale": "Show scale" + "show-scale": "Show scale", + "show-labels": "Show marker names" }, "table_context_menu": { "delete_row": "Delete row" @@ -2183,8 +2190,9 @@ "percentage": "%" }, "pagination": { - "page_title": "Page of {{startIndex}} - {{endIndex}}", - "total_notes": "{{count}} notes" + "total_notes": "{{count}} notes", + "prev_page": "Previous page", + "next_page": "Next page" }, "collections": { "rendering_error": "Unable to show content due to an error." @@ -2288,5 +2296,32 @@ }, "bookmark_buttons": { "bookmarks": "Bookmarks" + }, + "active_content_badges": { + "type_icon_pack": "Icon pack", + "type_backend_script": "Backend script", + "type_frontend_script": "Frontend script", + "type_widget": "Widget", + "type_app_css": "Custom CSS", + "type_render_note": "Render note", + "type_web_view": "Web view", + "type_app_theme": "Custom theme", + "toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.", + "toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.", + "menu_docs": "Open documentation", + "menu_execute_now": "Execute script now", + "menu_run": "Run automatically", + "menu_run_disabled": "Manually", + "menu_run_backend_startup": "When the backend starts up", + "menu_run_hourly": "Hourly", + "menu_run_daily": "Daily", + "menu_run_frontend_startup": "When the desktop frontend starts up", + "menu_run_mobile_startup": "When the mobile frontend starts up", + "menu_change_to_widget": "Change to widget", + "menu_change_to_frontend_script": "Change to frontend script", + "menu_theme_base": "Theme base" + }, + "setup_form": { + "more_info": "Learn more" } } diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 57323d6d0c..a583bc2b0d 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -669,7 +669,7 @@ "button_exit": "Salir del modo Zen" }, "sync_status": { - "unknown": "

El estado de sincronización será conocido una vez que el siguiente intento de sincronización comience.

Dé clic para activar la sincronización ahora

", + "unknown": "

El estado de sincronización será conocido una vez que el siguiente intento de sincronización comience.

Dé clic para activar la sincronización ahora.

", "connected_with_changes": "

Conectado al servidor de sincronización.
Hay cambios pendientes que aún no se han sincronizado.

Dé clic para activar la sincronización.

", "connected_no_changes": "

Conectado al servidor de sincronización.
Todos los cambios ya han sido sincronizados.

Dé clic para activar la sincronización.

", "disconnected_with_changes": "

El establecimiento de la conexión con el servidor de sincronización no ha tenido éxito.
Hay algunos cambios pendientes que aún no se han sincronizado.

Dé clic para activar la sincronización.

", @@ -760,7 +760,7 @@ "mobile_detail_menu": { "insert_child_note": "Insertar subnota", "delete_this_note": "Eliminar esta nota", - "error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'", + "error_cannot_get_branch_id": "No se puede obtener el branchId del notePath '{{notePath}}'", "error_unrecognized_command": "Comando no reconocido {{command}}", "note_revisions": "Revisiones de notas", "backlinks": "Vínculos de retroceso", @@ -1012,7 +1012,7 @@ "no_attachments": "Esta nota no tiene archivos adjuntos." }, "book": { - "no_children_help": "Esta nota de tipo libro no tiene ninguna subnota así que no hay nada que mostrar. Véa la wiki para más detalles.", + "no_children_help": "Esta colección no tiene ninguna subnota así que no hay nada que mostrar.", "drag_locked_title": "Bloqueado para edición", "drag_locked_message": "No se permite Arrastrar pues la colección está bloqueada para edición." }, @@ -1068,10 +1068,6 @@ "default_new_note_title": "nueva nota", "click_on_canvas_to_place_new_note": "Haga clic en el lienzo para colocar una nueva nota" }, - "render": { - "note_detail_render_help_1": "Esta nota de ayuda se muestra porque esta nota de tipo Renderizar HTML no tiene la relación requerida para funcionar correctamente.", - "note_detail_render_help_2": "El tipo de nota Render HTML es usado para scripting. De forma resumida, tiene una nota con código HTML (opcionalmente con algo de JavaScript) y esta nota la renderizará. Para que funcione, es necesario definir una relación llamada \"renderNote\" apuntando a la nota HTML nota a renderizar." - }, "backend_log": { "refresh": "Refrescar" }, @@ -1564,7 +1560,7 @@ "shortcuts": { "keyboard_shortcuts": "Atajos de teclado", "multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.", - "electron_documentation": "Véa la documentación de Electron para los modificadores y códigos de tecla disponibles.", + "electron_documentation": "Consulte la documentación de Electron para los modificadores y códigos de tecla disponibles.", "type_text_to_filter": "Escriba texto para filtrar los accesos directos...", "action_name": "Nombre de la acción", "shortcuts": "Atajos", @@ -1830,7 +1826,7 @@ "no_headings": "Sin encabezados." }, "watched_file_update_status": { - "file_last_modified": "Archivo ha sido modificado por última vez en.", + "file_last_modified": "El archivo ha sido modificado por última vez en .", "upload_modified_file": "Subir archivo modificado", "ignore_this_change": "Ignorar este cambio" }, @@ -2054,7 +2050,8 @@ "max-nesting-depth": "Máxima profundidad de anidamiento:", "vector_light": "Vector (claro)", "vector_dark": "Vector (oscuro)", - "raster": "Trama" + "raster": "Trama", + "show-labels": "Mostrar nombres de marcadores" }, "table_context_menu": { "delete_row": "Eliminar fila" @@ -2163,7 +2160,8 @@ }, "pagination": { "total_notes": "{{count}} notas", - "page_title": "Página de {{startIndex}} - {{endIndex}}" + "prev_page": "Página anterior", + "next_page": "Página siguiente" }, "presentation_view": { "edit-slide": "Editar este slide", @@ -2302,6 +2300,43 @@ "url_placeholder": "Ingresar o pegar la dirección del sitio web, por ejemplo https://triliumnotes.org", "create_button": "Crear Vista Web", "invalid_url_title": "Dirección inválida", - "invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org." + "invalid_url_message": "Ingrese una dirección web válida, por ejemplo https://triliumnotes.org.", + "disabled_description": "Esta vista web fue importada de una fuente externa. Para ayudarlo a protegerse del phishing o el contenido malicioso, no se está cargando automáticamente. Puede activarlo si confía en la fuente.", + "disabled_button_enable": "Habilita vista web" + }, + "render": { + "setup_title": "Mostrar HTML personalizado o Preact JSX dentro de esta nota", + "setup_create_sample_preact": "Crear nota de muestra con Preact", + "setup_create_sample_html": "Crear nota de muestra con HTML", + "setup_sample_created": "Se creó una nota de muestra como subnota.", + "disabled_description": "Esta nota de renderización proviene de una fuente externa. Para protegerlo de contenido malicioso, no está habilitado por defecto. Asegúrese de confiar en la fuente antes de habilitarla.", + "disabled_button_enable": "Habilitar nota de renderización" + }, + "active_content_badges": { + "type_icon_pack": "Paquete de iconos", + "type_backend_script": "Script de backend", + "type_frontend_script": "Script de frontend", + "type_widget": "Widget", + "type_app_css": "CSS personalizado", + "type_render_note": "Nota de renderización", + "type_web_view": "Vista web", + "type_app_theme": "Tema personalizado", + "toggle_tooltip_enable_tooltip": "Haga clic para habilitar este {{type}}.", + "toggle_tooltip_disable_tooltip": "Haga clic para deshabilitar este {{type}}.", + "menu_docs": "Abrir documentación", + "menu_execute_now": "Ejecutar script ahora", + "menu_run": "Ejecutar automáticamente", + "menu_run_disabled": "Manualmente", + "menu_run_backend_startup": "Cuando el backend inicia", + "menu_run_hourly": "Cada hora", + "menu_run_daily": "Diariamente", + "menu_run_frontend_startup": "Cuando el frontend de escritorio inicia", + "menu_run_mobile_startup": "Cuando el frontend móvil inicia", + "menu_change_to_widget": "Cambiar a widget", + "menu_change_to_frontend_script": "Cambiar a script de frontend", + "menu_theme_base": "Tema base" + }, + "setup_form": { + "more_info": "Para saber más" } } diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index b1253d8d81..69b4eee16a 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -1053,10 +1053,6 @@ "default_new_note_title": "nouvelle note", "click_on_canvas_to_place_new_note": "Cliquez sur le canevas pour placer une nouvelle note" }, - "render": { - "note_detail_render_help_1": "Cette note d'aide s'affiche car cette note de type Rendu HTML n'a pas la relation requise pour fonctionner correctement.", - "note_detail_render_help_2": "Le type de note Rendu HTML est utilisé pour les scripts. En résumé, vous disposez d'une note de code HTML (éventuellement contenant JavaScript) et cette note affichera le rendu. Pour que cela fonctionne, vous devez définir une relation appelée \"renderNote\" pointant vers la note HTML à rendre." - }, "backend_log": { "refresh": "Rafraîchir" }, @@ -2053,7 +2049,6 @@ "percentage": "%" }, "pagination": { - "page_title": "Page de {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} notes" }, "collections": { diff --git a/apps/client/src/translations/ga/translation.json b/apps/client/src/translations/ga/translation.json index 19029219d9..5592c8f3a9 100644 --- a/apps/client/src/translations/ga/translation.json +++ b/apps/client/src/translations/ga/translation.json @@ -442,7 +442,7 @@ "share_index": "liostálfaidh nóta leis an lipéad seo fréamhacha uile nótaí comhroinnte", "display_relations": "Ainmneacha caidrimh scartha le camóga ar cheart iad a thaispeáint. Beidh na cinn eile go léir i bhfolach.", "hide_relations": "Ainmneacha caidrimh scartha le camóga ar cheart iad a cheilt. Taispeánfar na cinn eile go léir.", - "title_template": "Teideal réamhshocraithe nótaí a cruthaíodh mar leanaí den nóta seo. Déantar an luach a mheas mar theaghrán JavaScript\n agus dá bhrí sin is féidir é a shaibhriú le hábhar dinimiciúil trí na hathróga now agus parentNote insteallta. Samplaí:\n\n
    \n
  • Saothair liteartha ${parentNote.getLabelValue('authorName')}
  • \n
  • Log le haghaidh ${now.format('YYYY-MM-DD HH:mm:ss')}
  • \n
\n\nFéach vicí le sonraí, doiciméid API le haghaidh parentNote agus now le haghaidh sonraí.", + "title_template": "teideal réamhshocraithe nótaí a cruthaíodh mar leanaí den nóta seo. Déantar an luach a mheas mar theaghrán JavaScript \n agus dá bhrí sin is féidir é a shaibhriú le hábhar dinimiciúil trí na hathróga now agus parentNote insteallta. Samplaí:\n \n
    \n
  • Saothair liteartha ${parentNote.getLabelValue('authorName')}
  • \n
  • Log le haghaidh ${now.format('YYYY-MM-DD HH:mm:ss')}
  • \n
\n \n Féach vicí le sonraí, doiciméid API le haghaidh parentNote agus now le haghaidh sonraí.", "template": "Beidh an nóta seo le feiceáil i roghnú na dteimpléad atá ar fáil agus nóta nua á chruthú", "toc": "Cuirfidh #toc#toc=show iallach ar an gClár Ábhair a bheith le feiceáil, cuirfidh #toc=hide iallach air é a cheilt. Mura bhfuil an lipéad ann, breathnaítear ar an socrú domhanda", "color": "sainmhíníonn dath an nóta sa chrann nótaí, snaisc srl. Úsáid aon luach datha CSS bailí cosúil le 'dearg' nó #a13d5f", @@ -1016,7 +1016,7 @@ "no_attachments": "Níl aon cheangaltáin leis an nóta seo." }, "book": { - "no_children_help": "Níl aon nótaí faoi mhíbhuntáiste sa bhailiúchán seo mar sin níl aon rud le taispeáint. Féach ar an vicí le haghaidh tuilleadh sonraí.", + "no_children_help": "Níl aon nótaí faoi mhíbhuntáiste sa bhailiúchán seo mar sin níl aon rud le taispeáint.", "drag_locked_title": "Glasáilte le haghaidh eagarthóireachta", "drag_locked_message": "Ní cheadaítear tarraingt ós rud é go bhfuil an bailiúchán faoi ghlas le haghaidh eagarthóireachta." }, @@ -1072,10 +1072,6 @@ "default_new_note_title": "nóta nua", "click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur" }, - "render": { - "note_detail_render_help_1": "Taispeántar an nóta cabhrach seo mar nach bhfuil aon ghaol riachtanach ag an nóta seo den chineál Render HTML le go bhfeidhmeoidh sé i gceart.", - "note_detail_render_help_2": "Úsáidtear cineál nóta HTML rindreála le haghaidh scriptithe. Go hachomair, tá nóta cóid HTML agat (le roinnt JavaScript más féidir) agus déanfaidh an nóta seo é a rindreáil. Chun go n-oibreoidh sé, ní mór duit gaol ar a dtugtar \"renderNote\" a shainiú ag pointeáil chuig an nóta HTML atá le rindreáil." - }, "backend_log": { "refresh": "Athnuachan" }, @@ -1590,7 +1586,8 @@ "description": "Cur síos", "reload_app": "Athlódáil an aip chun na hathruithe a chur i bhfeidhm", "set_all_to_default": "Socraigh gach aicearra go dtí an réamhshocrú", - "confirm_reset": "An bhfuil tú cinnte gur mhaith leat na haicearraí méarchláir go léir a athshocrú go dtí an rogha réamhshocraithe?" + "confirm_reset": "An bhfuil tú cinnte gur mhaith leat na haicearraí méarchláir go léir a athshocrú go dtí an rogha réamhshocraithe?", + "no_results": "Níor aimsíodh aon aicearraí a mheaitseálann '{{filter}}'" }, "spellcheck": { "title": "Seiceáil Litrithe", @@ -1808,7 +1805,9 @@ "print_report_collection_content_many": "Níorbh fhéidir {{count}} nótaí sa bhailiúchán a phriontáil mar nach dtacaítear leo nó mar go bhfuil siad faoi chosaint.", "print_report_collection_content_other": "Níorbh fhéidir {{count}} nótaí sa bhailiúchán a phriontáil mar nach dtacaítear leo nó mar go bhfuil siad faoi chosaint.", "print_report_collection_details_button": "Féach sonraí", - "print_report_collection_details_ignored_notes": "Nótaí neamhairdithe" + "print_report_collection_details_ignored_notes": "Nótaí neamhairdithe", + "print_report_error_title": "Theip ar phriontáil", + "print_report_stack_trace": "Rian cruachta" }, "note_title": { "placeholder": "clóscríobh teideal an nóta anseo...", @@ -2109,7 +2108,8 @@ "raster": "Raster", "vector_light": "Veicteoir (Solas)", "vector_dark": "Veicteoir (Dorcha)", - "show-scale": "Taispeáin scála" + "show-scale": "Taispeáin scála", + "show-labels": "Taispeáin ainmneacha marcóirí" }, "table_context_menu": { "delete_row": "Scrios an tsraith" @@ -2188,8 +2188,9 @@ "percentage": "%" }, "pagination": { - "page_title": "Leathanach de {{startIndex}} - {{endIndex}}", - "total_notes": "{{count}} nótaí" + "total_notes": "{{count}} nótaí", + "prev_page": "Leathanach roimhe seo", + "next_page": "An chéad leathanach eile" }, "collections": { "rendering_error": "Ní féidir ábhar a thaispeáint mar gheall ar earráid." @@ -2329,6 +2330,43 @@ "url_placeholder": "Cuir isteach nó greamaigh seoladh an tsuímh ghréasáin, mar shampla https://triliumnotes.org", "create_button": "Cruthaigh Radharc Gréasáin", "invalid_url_title": "Seoladh neamhbhailí", - "invalid_url_message": "Cuir isteach seoladh gréasáin bailí, mar shampla https://triliumnotes.org." + "invalid_url_message": "Cuir isteach seoladh gréasáin bailí, mar shampla https://triliumnotes.org.", + "disabled_description": "Iompórtáladh an radharc gréasáin seo ó fhoinse sheachtrach. Chun cabhrú leat a chosaint ar ábhar fioscaireachta nó mailíseach, níl sé ag lódáil go huathoibríoch. Is féidir leat é a chumasú má tá muinín agat as an bhfoinse.", + "disabled_button_enable": "Cumasaigh radharc gréasáin" + }, + "render": { + "setup_title": "Taispeáin HTML saincheaptha nó Preact JSX taobh istigh den nóta seo", + "setup_create_sample_preact": "Cruthaigh nóta samplach le Preact", + "setup_create_sample_html": "Cruthaigh nóta samplach le HTML", + "setup_sample_created": "Cruthaíodh nóta samplach mar nóta linbh.", + "disabled_description": "Tagann na nótaí rindreála seo ó fhoinse sheachtrach. Chun tú a chosaint ar ábhar mailíseach, níl sé cumasaithe de réir réamhshocraithe. Déan cinnte go bhfuil muinín agat as an bhfoinse sula gcumasaíonn tú é.", + "disabled_button_enable": "Cumasaigh nóta rindreála" + }, + "active_content_badges": { + "type_icon_pack": "Pacáiste deilbhín", + "type_backend_script": "Script chúltaca", + "type_frontend_script": "Script tosaigh", + "type_widget": "Giuirléid", + "type_app_css": "CSS saincheaptha", + "type_render_note": "Nóta rindreála", + "type_web_view": "Radharc gréasáin", + "type_app_theme": "Téama saincheaptha", + "toggle_tooltip_enable_tooltip": "Cliceáil chun an {{type}} seo a chumasú.", + "toggle_tooltip_disable_tooltip": "Cliceáil chun an {{type}} seo a dhíchumasú.", + "menu_docs": "Doiciméadú oscailte", + "menu_execute_now": "Rith an script anois", + "menu_run": "Rith go huathoibríoch", + "menu_run_disabled": "De láimh", + "menu_run_backend_startup": "Nuair a thosaíonn an cúltaca", + "menu_run_hourly": "Gach uair an chloig", + "menu_run_daily": "Laethúil", + "menu_run_frontend_startup": "Nuair a thosaíonn tosaigh an deisce", + "menu_run_mobile_startup": "Nuair a thosaíonn an taobhlíne soghluaiste", + "menu_change_to_widget": "Athraigh go giuirléid", + "menu_change_to_frontend_script": "Athraigh chuig an script tosaigh", + "menu_theme_base": "Bunús téama" + }, + "setup_form": { + "more_info": "Foghlaim níos mó" } } diff --git a/apps/client/src/translations/id/translation.json b/apps/client/src/translations/id/translation.json index 8b9e90cabd..157f406053 100644 --- a/apps/client/src/translations/id/translation.json +++ b/apps/client/src/translations/id/translation.json @@ -72,7 +72,8 @@ "ok": "Oke", "are_you_sure_remove_note": "Apakah anda yakin mau membuang catatan \"{{title}}\" dari peta relasi? ", "if_you_dont_check": "Jika Anda tidak mencentang ini, catatan hanya akan dihapus dari peta relasi.", - "also_delete_note": "Hapus juga catatannya" + "also_delete_note": "Hapus juga catatannya", + "confirmation": "Konfirmasi" }, "delete_notes": { "delete_notes_preview": "Hapus pratinjau catatan", @@ -81,11 +82,19 @@ "erase_notes_description": "Penghapusan normal hanya menandai catatan sebagai dihapus dan dapat dipulihkan (melalui dialog versi revisi) dalam jangka waktu tertentu. Mencentang opsi ini akan menghapus catatan secara permanen seketika dan catatan tidak akan bisa dipulihkan kembali.", "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)." + "no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat).", + "broken_relations_to_be_deleted": "Hubungan berikut akan diputus dan dihapus ({{ relationCount}})" }, "clone_to": { "clone_notes_to": "Duplikat catatan ke…", "help_on_links": "Bantuan pada tautan", - "notes_to_clone": "Catatan untuk kloning" + "notes_to_clone": "Catatan untuk kloning", + "target_parent_note": "Sasaran catatan utama", + "search_for_note_by_its_name": "cari catatan berdasarkan namanya", + "cloned_note_prefix_title": "Catatan yang dikloning akan ditampilkan diruntutan catatan dengan awalan yang diberikan", + "prefix_optional": "Awalan (opsional)", + "clone_to_selected_note": "Salin ke catatan yang dipilih", + "no_path_to_clone_to": "Tidak ada jalur untuk digandakan.", + "note_cloned": "Catatan \"{{clonedTitle}}\" telah digandakan ke dalam \"{{targetTitle}}\"" } } diff --git a/apps/client/src/translations/it/translation.json b/apps/client/src/translations/it/translation.json index d994317a82..f24d449fcb 100644 --- a/apps/client/src/translations/it/translation.json +++ b/apps/client/src/translations/it/translation.json @@ -167,8 +167,8 @@ "desktop-application": "Applicazione Desktop", "native-title-bar": "Barra del titolo nativa", "native-title-bar-description": "Su Windows e macOS, disattivare la barra del titolo nativa rende l'applicazione più compatta. Su Linux, attivarla si integra meglio con il resto del sistema.", - "background-effects": "Abilita effetti di sfondo (solo Windows 11)", - "background-effects-description": "L'effetto Mica aggiunge uno sfondo sfocato ed elegante alle finestre delle app, creando profondità e un aspetto moderno. La \"Barra del titolo nativa\" deve essere disattivata.", + "background-effects": "Abilita effetti di sfondo", + "background-effects-description": "Aggiunge uno sfondo sfocato ed elegante alle finestre dell'app, creando profondità e un look moderno. La \"barra del titolo nativa\" deve essere disabilitata.", "restart-app-button": "Riavviare l'applicazione per visualizzare le modifiche" }, "note_autocomplete": { @@ -369,7 +369,8 @@ "description": "Descrizione", "reload_app": "Ricarica l'app per applicare le modifiche", "set_all_to_default": "Imposta tutte le scorciatoie sui valori predefiniti", - "confirm_reset": "Vuoi davvero ripristinare tutte le scorciatoie da tastiera ai valori predefiniti?" + "confirm_reset": "Vuoi davvero ripristinare tutte le scorciatoie da tastiera ai valori predefiniti?", + "no_results": "Nessuna scorciatoia trovata corrispondente '{{filter}}'" }, "shared_switch": { "toggle-on-title": "Condividi la nota", @@ -1327,7 +1328,7 @@ "button_title": "Esporta diagramma come SVG" }, "relation_map_buttons": { - "create_child_note_title": "Crea una nuova nota secondaria e aggiungila a questa mappa delle relazioni", + "create_child_note_title": "Crea una nota secondaria e aggiungila alla mappa", "reset_pan_zoom_title": "Ripristina panoramica e zoom alle coordinate e all'ingrandimento iniziali", "zoom_in_title": "Ingrandisci", "zoom_out_title": "Rimpicciolisci" @@ -1526,7 +1527,7 @@ "no_attachments": "Questa nota non ha allegati." }, "book": { - "no_children_help": "Questa raccolta non ha note secondarie, quindi non c'è nulla da visualizzare. Consulta la wiki per i dettagli.", + "no_children_help": "Questa raccolta non ha note secondarie, quindi non c'è nulla da visualizzare.", "drag_locked_title": "Bloccato per la modifica", "drag_locked_message": "Trascinamento non consentito poiché la raccolta è bloccata per la modifica." }, @@ -1582,10 +1583,6 @@ "default_new_note_title": "nuova nota", "click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota" }, - "render": { - "note_detail_render_help_1": "Questa nota di aiuto viene visualizzata perché questa nota di tipo Render HTML non ha la relazione richiesta per funzionare correttamente.", - "note_detail_render_help_2": "Il tipo di nota HTML Render viene utilizzato per lo scripting. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una relazione denominata \"renderNote\" che punti alla nota HTML da visualizzare." - }, "vacuum_database": { "title": "Pulizia del database", "description": "Questa operazione ricostruirà il database, generando in genere un file di dimensioni inferiori. In realtà, nessun dato verrà modificato.", @@ -1923,7 +1920,9 @@ "print_report_collection_content_many": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.", "print_report_collection_content_other": "{{count}} le note nella raccolta non possono essere stampate perché non sono supportate o sono protette.", "print_report_collection_details_button": "Vedi dettagli", - "print_report_collection_details_ignored_notes": "Note ignorate" + "print_report_collection_details_ignored_notes": "Note ignorate", + "print_report_error_title": "Impossibile stampare", + "print_report_stack_trace": "Traccia dello stack" }, "note_title": { "placeholder": "scrivi qui il titolo della nota...", @@ -2110,7 +2109,8 @@ "raster": "Trama", "vector_light": "Vettore (Luce)", "vector_dark": "Vettore (scuro)", - "show-scale": "Mostra scala" + "show-scale": "Mostra scala", + "show-labels": "Mostra nomi dei marcatori" }, "table_context_menu": { "delete_row": "Elimina riga" @@ -2143,7 +2143,7 @@ "next_theme_message": "Al momento stai utilizzando il tema legacy. Vuoi provare il nuovo tema?", "next_theme_button": "Prova il nuovo tema", "background_effects_title": "Gli effetti di sfondo sono ora stabili", - "background_effects_message": "Sui dispositivi Windows, gli effetti di sfondo sono ora completamente stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo retrostante. Questa tecnica è utilizzata anche in altre applicazioni come Esplora risorse di Windows.", + "background_effects_message": "Su dispositivi Windows e macOS, gli effetti di sfondo sono ora stabili. Gli effetti di sfondo aggiungono un tocco di colore all'interfaccia utente sfocando lo sfondo dietro di essa.", "background_effects_button": "Abilita gli effetti di sfondo", "dismiss": "Chiudi", "new_layout_title": "Nuovo layout", @@ -2164,7 +2164,6 @@ "percentage": "%" }, "pagination": { - "page_title": "Pagina di {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} note" }, "collections": { @@ -2287,7 +2286,9 @@ "url_placeholder": "Inserisci o incolla l'indirizzo del sito web, ad esempio https://triliumnotes.org", "create_button": "Crea vista Web", "invalid_url_title": "Indirizzo non valido", - "invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org." + "invalid_url_message": "Inserisci un indirizzo web valido, ad esempio https://triliumnotes.org.", + "disabled_description": "Questa visualizzazione web è stata importata da una fonte esterna. Per proteggerti dal phishing o da contenuti dannosi, non viene caricata automaticamente. Puoi abilitarla se ritieni che la fonte sia affidabile.", + "disabled_button_enable": "Abilita visualizzazione web" }, "platform_indicator": { "available_on": "Disponibile su {{platform}}" @@ -2300,5 +2301,40 @@ }, "bookmark_buttons": { "bookmarks": "Segnalibri" + }, + "render": { + "setup_title": "Visualizza HTML personalizzato o Preact JSX all'interno di questa nota", + "setup_create_sample_preact": "Crea una nota di esempio con Preact", + "setup_create_sample_html": "Crea una nota di esempio con HTML", + "setup_sample_created": "È stata creata una nota di esempio come nota secondaria.", + "disabled_description": "Queste note di rendering provengono da una fonte esterna. Per proteggerti da contenuti dannosi, non sono abilitate per impostazione predefinita. Assicurati di fidarti della fonte prima di abilitarle.", + "disabled_button_enable": "Abilita nota di rendering" + }, + "active_content_badges": { + "type_icon_pack": "Pacchetto icone", + "type_backend_script": "Script di backend", + "type_frontend_script": "Script frontend", + "type_widget": "Widget", + "type_app_css": "CSS personalizzato", + "type_render_note": "Nota di rendering", + "type_web_view": "Visualizzazione web", + "type_app_theme": "Tema personalizzato", + "toggle_tooltip_enable_tooltip": "Clicca per abilitare questa funzione {{type}}.", + "toggle_tooltip_disable_tooltip": "Clicca per disattivare questa funzione {{type}}.", + "menu_docs": "Documentazione aperta", + "menu_execute_now": "Esegui lo script ora", + "menu_run": "Esegui automaticamente", + "menu_run_disabled": "Manualmente", + "menu_run_backend_startup": "Quando il backend si avvia", + "menu_run_hourly": "Ogni ora", + "menu_run_daily": "Giornaliero", + "menu_run_frontend_startup": "Quando si avvia il frontend desktop", + "menu_run_mobile_startup": "Quando si avvia il frontend mobile", + "menu_change_to_widget": "Passa al widget", + "menu_change_to_frontend_script": "Modifica allo script frontend", + "menu_theme_base": "Tema base" + }, + "setup_form": { + "more_info": "Per saperne di più" } } diff --git a/apps/client/src/translations/ja/translation.json b/apps/client/src/translations/ja/translation.json index abf330b0d3..618166ad00 100644 --- a/apps/client/src/translations/ja/translation.json +++ b/apps/client/src/translations/ja/translation.json @@ -1860,10 +1860,6 @@ "protecting-title": "保護の状態", "unprotecting-title": "保護解除の状態" }, - "render": { - "note_detail_render_help_1": "このヘルプノートが表示されるのは、このノートの「HTML のレンダリング」タイプには、正常に機能するために必要なリレーションがないためです。", - "note_detail_render_help_2": "レンダリングHTMLノートタイプは、スクリプティングに使用されます。簡単に言うと、HTMLコードノート(オプションでJavaScriptを含む)があり、このノートがそれをレンダリングします。これを動作させるには、レンダリングするHTMLノートを指す「renderNote」というリレーションを定義する必要があります。" - }, "consistency_checks": { "find_and_fix_button": "一貫性の問題を見つけて修正する", "finding_and_fixing_message": "一貫性の問題を見つけて修正中…", @@ -2040,7 +2036,8 @@ "show-scale": "スケールを表示", "raster": "Raster", "vector_light": "Vector(ライト)", - "vector_dark": "Vector (ダーク)" + "vector_dark": "Vector (ダーク)", + "show-labels": "マーカー名を表示" }, "call_to_action": { "next_theme_title": "新しいTriliumテーマをお試しください", @@ -2068,8 +2065,9 @@ "percentage": "%" }, "pagination": { - "page_title": "{{startIndex}} - {{endIndex}} ページ", - "total_notes": "{{count}} ノート" + "total_notes": "{{count}} ノート", + "prev_page": "前のページ", + "next_page": "次のページ" }, "collections": { "rendering_error": "エラーのためコンテンツを表示できません。" @@ -2097,7 +2095,7 @@ "no_attachments": "このノートには添付ファイルはありません。" }, "book": { - "no_children_help": "このコレクションには子ノートがないため、表示するものがありません。詳細はwikiをご覧ください。", + "no_children_help": "このコレクションには子ノートがないため、表示するものがありません。", "drag_locked_title": "編集をロック中", "drag_locked_message": "コレクションは編集がロックされているため、ドラッグは許可されていません。" }, @@ -2272,6 +2270,43 @@ "url_placeholder": "Web サイトのアドレスを入力または貼り付けて下さい。 例: https://triliumnotes.org", "create_button": "Web ビューを作成", "invalid_url_title": "無効なアドレス", - "invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org" + "invalid_url_message": "有効な Web アドレスを入力してください。 例: https://triliumnotes.org", + "disabled_description": "この Web ビューは外部ソースからインポートされました。フィッシングや悪意のあるコンテンツから保護するため、自動的には読み込まれません。ソースを信頼できる場合は、有効にすることができます。", + "disabled_button_enable": "Web ビューを有効" + }, + "render": { + "setup_title": "このノート内にカスタム HTML または Preact JSX を表示", + "setup_create_sample_preact": "Preact でサンプルノートを作成", + "setup_create_sample_html": "HTML でサンプルノートを作成", + "setup_sample_created": "子ノートとしてサンプルノートが作成されました。", + "disabled_description": "このレンダリングノートは外部ソースから提供されています。悪意のあるコンテンツからユーザーを保護するため、デフォルトでは有効になっていません。有効にする前に、ソースが信頼できるかどうかをご確認ください。", + "disabled_button_enable": "レンダリングノートを有効" + }, + "active_content_badges": { + "type_icon_pack": "アイコンパック", + "type_backend_script": "バックエンドスクリプト", + "type_frontend_script": "フロントエンドスクリプト", + "type_widget": "ウィジェット", + "type_app_css": "カスタム CSS", + "type_render_note": "レンダリングノート", + "type_web_view": "Web ビュー", + "type_app_theme": "カスタムテーマ", + "toggle_tooltip_enable_tooltip": "この {{type}} を有効にするにはクリックしてください。", + "toggle_tooltip_disable_tooltip": "この {{type}} を無効にするにはクリックしてください。", + "menu_docs": "ドキュメントを開く", + "menu_execute_now": "今すぐスクリプトを実行", + "menu_run": "自動で実行", + "menu_run_disabled": "手動で実行", + "menu_run_backend_startup": "バックエンドの起動時", + "menu_run_hourly": "毎時", + "menu_run_daily": "毎日", + "menu_run_frontend_startup": "デスクトップ フロントエンドの起動時", + "menu_run_mobile_startup": "モバイル フロントエンドの起動時", + "menu_change_to_widget": "ウィジェットの変更", + "menu_change_to_frontend_script": "フロントエンドスクリプトの変更", + "menu_theme_base": "テーマベース" + }, + "setup_form": { + "more_info": "さらに詳しく" } } diff --git a/apps/client/src/translations/ko/translation.json b/apps/client/src/translations/ko/translation.json index aefb75c6ef..bf4ac7b6bd 100644 --- a/apps/client/src/translations/ko/translation.json +++ b/apps/client/src/translations/ko/translation.json @@ -21,8 +21,17 @@ }, "bundle-error": { "title": "사용자 정의 스크립트를 불러오는데 실패했습니다", - "message": "ID가 \"{{id}}\"고, 제목이 \"{{title}}\"인 노트에서 스크립트가 실행되지 못했습니다:\n\n{{message}}" - } + "message": "다음 이유로 인해 스크립트가 실행되지 못했습니다:\n\n{{message}}" + }, + "widget-list-error": { + "title": "서버에서 위젯 목록을 가져오는 데 실패했습니다" + }, + "widget-render-error": { + "title": "사용자 정의 React 위젯을 렌더링하는 데 실패했습니다" + }, + "widget-missing-parent": "사용자 정의 위젯에 필수 속성 '{{property}}'가 정의되어 있지 않습니다.\n\n이 스크립트를 UI 요소 없이 실행하려면 '#run=frontendStartup'을 대신 사용하십시오.", + "open-script-note": "스크립트 노트 열기", + "scripting-error": "사용자 지정 스크립트 오류: {{title}}" }, "add_link": { "add_link": "링크 추가", @@ -41,7 +50,8 @@ "prefix": "접두사: ", "branch_prefix_saved": "브랜치 접두사가 저장되었습니다.", "edit_branch_prefix_multiple": "{{count}}개의 지점 접두사 편집", - "branch_prefix_saved_multiple": "{{count}}개의 지점에 대해 지점 접두사가 저장되었습니다." + "branch_prefix_saved_multiple": "{{count}}개의 지점에 대해 지점 접두사가 저장되었습니다.", + "affected_branches": "영향을 받는 브랜치 수 ({{count}}):" }, "bulk_actions": { "bulk_actions": "대량 작업", @@ -64,10 +74,66 @@ "first-week-contains-first-day": "첫 번째 주에는 올해의 첫날이 포함됩니다" }, "clone_to": { - "clone_notes_to": "~로 노트 복제", + "clone_notes_to": "노트 클론하기...", "help_on_links": "링크에 대한 도움말", "notes_to_clone": "노트 클론 생성", "target_parent_note": "부모 노트 타겟", - "search_for_note_by_its_name": "이름으로 노트 검색하기" + "search_for_note_by_its_name": "이름으로 노트 검색하기", + "no_path_to_clone_to": "클론할 경로가 존재하지 않습니다.", + "note_cloned": "노트 \"{{clonedTitle}}\"이(가) \"{{targetTitle}}\"로 클론되었습니다", + "cloned_note_prefix_title": "클론된 노트는 지정된 접두사와 함께 노트 트리에 표시됩니다", + "prefix_optional": "접두사 (선택 사항)", + "clone_to_selected_note": "선택한 노트에 클론" + }, + "confirm": { + "confirmation": "확인", + "cancel": "취소", + "ok": "OK", + "are_you_sure_remove_note": "관계 맵에서 \"{{title}}\" 노트를 정말로 제거하시겠습니까? ", + "if_you_dont_check": "이 항목을 선택하지 않으면 해당 노트는 관계 맵에서만 제거됩니다.", + "also_delete_note": "노트를 함께 삭제" + }, + "delete_notes": { + "erase_notes_description": "일반(소프트) 삭제는 메모를 삭제된 것으로 표시하는 것일 뿐이며, 일정 시간 동안 (최근 변경 내용 대화 상자에서) 복구할 수 있습니다. 이 옵션을 선택하면 메모가 즉시 삭제되며 복구할 수 없습니다.", + "erase_notes_warning": "모든 복제본을 포함하여 메모를 영구적으로 삭제합니다(이 작업은 되돌릴 수 없습니다). 애플리케이션이 다시 시작됩니다.", + "notes_to_be_deleted": "다음 노트가 삭제됩니다 ({{notesCount}})", + "no_note_to_delete": "삭제되는 노트가 없습니다 (클론만 삭제됩니다).", + "broken_relations_to_be_deleted": "다음 관계가 끊어지고 삭제됩니다({{ relationCount}})", + "cancel": "취소", + "ok": "OK", + "deleted_relation_text": "삭제 예정인 노트 {{- note}} (은)는 {{- source}}에서 시작된 관계 {{- relation}}에 의해 참조되고 있습니다.", + "delete_notes_preview": "노트 미리보기 삭제", + "close": "닫기", + "delete_all_clones_description": "모든 복제본 삭제(최근 변경 사항에서 되돌릴 수 있습니다)" + }, + "export": { + "export_note_title": "노트 내보내기", + "export_type_single": "이 노트에만 해당(후손 노트를 포함하지 않음)", + "export": "내보내기", + "choose_export_type": "내보내기 타입을 선택해 주세요", + "export_status": "상태 내보내기", + "export_in_progress": "내보내기 진행 중: {{progressCount}}", + "export_finished_successfully": "내보내기를 성공적으로 완료했습니다.", + "format_pdf": "PDF - 인쇄 또는 공유용", + "share-format": "웹 게시용 HTML - 공유 노트에 사용되는 것과 동일한 테마를 사용하지만 정적 웹사이트로 게시할 수 있습니다.", + "close": "닫기", + "export_type_subtree": "이 노트와 모든 후손 노트", + "format_html": "HTML - 모든 형식 유지됨, 권장", + "format_html_zip": "HTML(ZIP 아카이브) - 모든 서식이 유지됨, 권장.", + "format_markdown": "마크다운 - 대부분의 서식이 유지됩니다.", + "format_opml": "OPML은 텍스트 전용 아웃라이너 교환 형식입니다. 서식, 이미지 및 파일은 포함되지 않습니다.", + "opml_version_1": "OPML v1.0 - 일반 텍스트만", + "opml_version_2": "OPML v2.0 - HTML 지원" + }, + "help": { + "title": "치트 시트", + "editShortcuts": "키보드 단축키 편집", + "noteNavigation": "노트 내비게이션", + "goUpDown": "노트 목록에서 위/아래로 이동", + "collapseExpand": "노트 접기/펼치기", + "notSet": "미설정", + "goBackForwards": "히스토리에서 뒤로/앞으로 이동", + "showJumpToNoteDialog": "\"노트로 이동\" 대화 상자 표시", + "scrollToActiveNote": "활성화된 노트로 스크롤 이동" } } diff --git a/apps/client/src/translations/pl/translation.json b/apps/client/src/translations/pl/translation.json index 3f8174ada8..2c690b0370 100644 --- a/apps/client/src/translations/pl/translation.json +++ b/apps/client/src/translations/pl/translation.json @@ -261,7 +261,6 @@ "percentage": "%" }, "pagination": { - "page_title": "Strona {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} notatek" }, "collections": { @@ -1432,10 +1431,6 @@ "default_new_note_title": "nowa notatka", "click_on_canvas_to_place_new_note": "Kliknij na płótnie, aby umieścić nową notatkę" }, - "render": { - "note_detail_render_help_1": "Ta notatka pomocy jest wyświetlana, ponieważ ta notatka typu Render HTML nie ma wymaganej relacji do poprawnego działania.", - "note_detail_render_help_2": "Typ notatki Render HTML jest używany do skryptowania. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować relację o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania." - }, "backend_log": { "refresh": "Odśwież" }, diff --git a/apps/client/src/translations/pt/translation.json b/apps/client/src/translations/pt/translation.json index 510ec70de7..7975182509 100644 --- a/apps/client/src/translations/pt/translation.json +++ b/apps/client/src/translations/pt/translation.json @@ -1064,10 +1064,6 @@ "default_new_note_title": "nova nota", "click_on_canvas_to_place_new_note": "Clique no quadro para incluir uma nova nota" }, - "render": { - "note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.", - "note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para automação. Em suma, tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, deve definir uma relação chamada \"renderNote\" que aponta para a nota HTML a ser renderizada." - }, "backend_log": { "refresh": "Recarregar" }, @@ -2169,7 +2165,6 @@ "delete_note": "Apagar nota..." }, "pagination": { - "page_title": "Página {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} notas" }, "collections": { diff --git a/apps/client/src/translations/pt_br/translation.json b/apps/client/src/translations/pt_br/translation.json index 99b5f94eb5..2c57dab23b 100644 --- a/apps/client/src/translations/pt_br/translation.json +++ b/apps/client/src/translations/pt_br/translation.json @@ -1991,10 +1991,6 @@ "drag_locked_title": "Bloqueado para edição", "drag_locked_message": "Arrastar não é permitido pois a coleção está bloqueada para edição." }, - "render": { - "note_detail_render_help_1": "Esta nota de ajuda é mostrada porque esta nota do tipo Renderizar HTML não possui a relação necessária para funcionar corretamente.", - "note_detail_render_help_2": "O tipo de nota Renderizar HTML é usado para automação. Em suma, você tem uma nota de código HTML (opcionalmente com algum JavaScript) e esta nota irá renderizá-la. Para fazê-lo funcionar, você precisa definir uma relação chamada \"renderNote\" apontando para a nota HTML a ser renderizada." - }, "etapi": { "title": "ETAPI", "description": "ETAPI é uma API REST usada para acessar a instância do Trilium programaticamente, sem interface gráfica.", @@ -2119,7 +2115,6 @@ "shared_locally": "Esta nota é compartilhada localmente em {{- link}}." }, "pagination": { - "page_title": "Página de {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} notas" }, "collections": { diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 8f6d8d0a6c..e955dab73b 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -1094,10 +1094,6 @@ "rename_relation_from": "Redenumește relația din", "to": "În" }, - "render": { - "note_detail_render_help_1": "Această notă informativă este afișată deoarece această notiță de tip „Randare HTML” nu are relația necesară pentru a funcționa corespunzător.", - "note_detail_render_help_2": "Notița de tipul „Render HTML” este utilizată pentru scriptare. Pe scurt, se folosește o notiță de tip cod HTML (opțional cu niște JavaScript) și această notiță o va randa. Pentru a funcționa, trebuie definită o relație denumită „renderNote” ce indică notița HTML de randat." - }, "revisions": { "confirm_delete": "Doriți ștergerea acestei revizii?", "confirm_delete_all": "Doriți ștergerea tuturor reviziilor acestei notițe?", @@ -2155,7 +2151,6 @@ "percentage": "%" }, "pagination": { - "page_title": "Pagina pentru {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} notițe" }, "collections": { diff --git a/apps/client/src/translations/ru/translation.json b/apps/client/src/translations/ru/translation.json index 5cd6f94ecb..aa495eb287 100644 --- a/apps/client/src/translations/ru/translation.json +++ b/apps/client/src/translations/ru/translation.json @@ -2081,10 +2081,6 @@ "help-button": { "title": "Открыть соответствующую страницу справки" }, - "render": { - "note_detail_render_help_2": "Тип заметки «Рендер HTML» используется для скриптинга. Если коротко, у вас есть заметка с HTML-кодом (возможно, с добавлением JavaScript), и эта заметка её отобразит. Для этого необходимо определить отношение с именем «renderNote», указывающее на HTML-заметку для отрисовки.", - "note_detail_render_help_1": "Эта справочная заметка отображается, поскольку эта справка типа Render HTML не имеет необходимой связи для правильной работы." - }, "file": { "too_big": "В целях повышения производительности в режиме предварительного просмотра отображаются только первые {{maxNumChars}} символов файла. Загрузите файл и откройте его во внешнем браузере, чтобы увидеть всё содержимое.", "file_preview_not_available": "Предварительный просмотр файла недоступен для этого файла." @@ -2154,8 +2150,7 @@ "rendering_error": "Невозможно отобразить содержимое из-за ошибки." }, "pagination": { - "total_notes": "{{count}} заметок", - "page_title": "Страница {{startIndex}} - {{endIndex}}" + "total_notes": "{{count}} заметок" }, "status_bar": { "attributes_one": "{{count}} атрибут", diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index 7205bf7177..39d2f73d60 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -1007,7 +1007,7 @@ "no_attachments": "此筆記沒有附件。" }, "book": { - "no_children_help": "此類型為書籍的筆記沒有任何子筆記,因此沒有內容可顯示。請參閱 wiki 以了解詳情。", + "no_children_help": "此集合沒有任何子筆記,因此沒有內容可顯示。", "drag_locked_title": "鎖定編輯", "drag_locked_message": "無法拖曳,因為此集合已被鎖定編輯。" }, @@ -1063,10 +1063,6 @@ "default_new_note_title": "新筆記", "click_on_canvas_to_place_new_note": "點擊畫布以放置新筆記" }, - "render": { - "note_detail_render_help_1": "之所以顯示此說明筆記,是因為該類型的渲染 HTML 沒有設定好必須的關聯。", - "note_detail_render_help_2": "渲染筆記類型用於編寫 腳本。簡單說就是您可以寫HTML程式碼(或者加上一些JavaScript程式碼), 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 關聯 指向要呈現的 HTML 筆記。" - }, "backend_log": { "refresh": "重新整理" }, @@ -1377,7 +1373,8 @@ "description": "描述", "reload_app": "重新載入應用以套用更改", "set_all_to_default": "將所有快捷鍵重設為預設值", - "confirm_reset": "您確定要將所有鍵盤快捷鍵重設為預設值嗎?" + "confirm_reset": "您確定要將所有鍵盤快捷鍵重設為預設值嗎?", + "no_results": "未找到符合 '{{filter}}' 的捷徑" }, "spellcheck": { "title": "拼寫檢查", @@ -1581,7 +1578,9 @@ "print_report_collection_content_one": "集合中的 {{count}} 篇筆記無法列印,因為它們不被支援或受到保護。", "print_report_collection_content_other": "", "print_report_collection_details_button": "查看詳情", - "print_report_collection_details_ignored_notes": "忽略的筆記" + "print_report_collection_details_ignored_notes": "忽略的筆記", + "print_report_error_title": "列印失敗", + "print_report_stack_trace": "堆棧追蹤" }, "note_title": { "placeholder": "請輸入筆記標題...", @@ -2075,7 +2074,8 @@ "raster": "柵格", "vector_light": "向量(淺色)", "vector_dark": "向量(深色)", - "show-scale": "顯示比例尺" + "show-scale": "顯示比例尺", + "show-labels": "顯示標記名稱" }, "table_context_menu": { "delete_row": "刪除列" @@ -2154,7 +2154,6 @@ "app-restart-required": "(需要重啟程式以套用更改)" }, "pagination": { - "page_title": "第 {{startIndex}} - {{endIndex}} 頁", "total_notes": "{{count}} 筆記" }, "collections": { @@ -2278,5 +2277,49 @@ }, "bookmark_buttons": { "bookmarks": "書籤" + }, + "render": { + "setup_title": "在此筆記中顯示自訂 HTML 或 Preact JSX", + "setup_create_sample_preact": "使用 Preact 建立範例筆記", + "setup_create_sample_html": "使用 HTML 建立範例筆記", + "setup_sample_created": "已建立一個範例筆記作為子筆記。", + "disabled_description": "此渲染筆記來自外部來源。為保護您免受惡意內容侵害,此功能預設為停用狀態。啟用前請務必確認來源可信。", + "disabled_button_enable": "啟用渲染筆記" + }, + "web_view_setup": { + "title": "將網頁直接匯入 Trilium 建立即時預覽", + "url_placeholder": "輸入或貼上網站網址,例如 https://triliumnotes.org", + "create_button": "建立網頁檢視", + "invalid_url_title": "無效地址", + "invalid_url_message": "請輸入有效的網址,例如 https://triliumnotes.org。", + "disabled_description": "此網頁檢視來自外部來源。為協助保護您免受網路釣魚或惡意內容侵害,內容不會自動載入。若您信任來源,可手動啟用此功能。", + "disabled_button_enable": "啟用網頁檢視" + }, + "active_content_badges": { + "type_icon_pack": "圖示包", + "type_backend_script": "後端腳本", + "type_frontend_script": "前端腳本", + "type_widget": "元件", + "type_app_css": "自訂 CSS", + "type_render_note": "渲染筆記", + "type_web_view": "網頁顯示", + "type_app_theme": "自訂主題", + "toggle_tooltip_enable_tooltip": "點擊以啟用此 {{type}}。", + "toggle_tooltip_disable_tooltip": "點擊以停用此 {{type}}。", + "menu_docs": "打開文件", + "menu_execute_now": "立即執行腳本", + "menu_run": "自動執行", + "menu_run_disabled": "手動", + "menu_run_backend_startup": "當後端啟動時", + "menu_run_hourly": "每小時", + "menu_run_daily": "每日", + "menu_run_frontend_startup": "當桌面前端啟動時", + "menu_run_mobile_startup": "當移動前端啟動時", + "menu_change_to_widget": "更改為元件", + "menu_change_to_frontend_script": "更改為前端腳本", + "menu_theme_base": "主題基底" + }, + "setup_form": { + "more_info": "了解更多" } } diff --git a/apps/client/src/translations/uk/translation.json b/apps/client/src/translations/uk/translation.json index 032fd70393..c2c8a5588d 100644 --- a/apps/client/src/translations/uk/translation.json +++ b/apps/client/src/translations/uk/translation.json @@ -1130,10 +1130,6 @@ "default_new_note_title": "нова нотатка", "click_on_canvas_to_place_new_note": "Натисніть на полотно, щоб розмістити нову нотатку" }, - "render": { - "note_detail_render_help_1": "Ця довідка відображається, оскільки ця нотатка типу Render HTML не має необхідного зв'язку для належного функціонування.", - "note_detail_render_help_2": "Тип нотатки Render HTML використовується для скриптів. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити відношення під назвою \"renderNote\", яке вказує на нотатку HTML для відображення." - }, "backend_log": { "refresh": "Оновити" }, @@ -2067,7 +2063,6 @@ "app-restart-required": "(щоб зміни набули чинності, потрібен перезапуск програми)" }, "pagination": { - "page_title": "Сторінка {{startIndex}} - {{endIndex}}", "total_notes": "{{count}} нотаток" }, "collections": { diff --git a/apps/client/src/types-lib.d.ts b/apps/client/src/types-lib.d.ts index aa125f389d..4f942b8cb6 100644 --- a/apps/client/src/types-lib.d.ts +++ b/apps/client/src/types-lib.d.ts @@ -63,11 +63,13 @@ declare global { declare module "preact" { namespace JSX { + interface ElectronWebViewElement extends JSX.HTMLAttributes { + src: string; + class: string; + } + interface IntrinsicElements { - webview: { - src: string; - class: string; - } + webview: ElectronWebViewElement; } } } diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index 2e2a36e6ee..f7673901c1 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -119,7 +119,7 @@ declare global { setNote(noteId: string); } - var logError: (message: string, e?: Error | string) => void; + var logError: (message: string, e?: unknown) => void; var logInfo: (message: string) => void; var glob: CustomGlobals; //@ts-ignore diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index c49802a7a4..1b2cc8f8cc 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -1,6 +1,7 @@ import "./NoteDetail.css"; import clsx from "clsx"; +import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js"; import { isValidElement, VNode } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; @@ -355,6 +356,14 @@ export function checkFullHeight(noteContext: NoteContext | undefined, type: Exte // https://github.com/zadam/trilium/issues/2522 const isBackendNote = noteContext?.noteId === "_backendLog"; const isFullHeightNoteType = type && TYPE_MAPPINGS[type].isFullHeight; + + // Allow vertical centering when there are no results. + if (type === "book" && + [ "grid", "list" ].includes(noteContext.note?.getLabelValue("viewType") ?? "grid") && + !noteContext.note?.hasChildren()) { + return true; + } + return (!noteContext?.hasNoteList() && isFullHeightNoteType) || noteContext?.viewScope?.viewMode === "attachments" || isBackendNote; diff --git a/apps/client/src/widgets/PromotedAttributes.css b/apps/client/src/widgets/PromotedAttributes.css index 6e3c7795d1..ef15905192 100644 --- a/apps/client/src/widgets/PromotedAttributes.css +++ b/apps/client/src/widgets/PromotedAttributes.css @@ -16,6 +16,10 @@ body.mobile .promoted-attributes-widget { display: table; } +body.experimental-feature-new-layout .promoted-attributes-container { + max-height: unset; +} + .promoted-attribute-cell { display: flex; align-items: center; @@ -94,4 +98,4 @@ body.mobile .promoted-attributes-widget { background: rgba(0, 0, 0, 0.5); transform: rotate(45deg); pointer-events: none; -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/PromotedAttributes.tsx b/apps/client/src/widgets/PromotedAttributes.tsx index cbe5737256..1e0bdeb474 100644 --- a/apps/client/src/widgets/PromotedAttributes.tsx +++ b/apps/client/src/widgets/PromotedAttributes.tsx @@ -3,14 +3,13 @@ import "./PromotedAttributes.css"; import { UpdateAttributeResponse } from "@triliumnext/commons"; import clsx from "clsx"; import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact"; -import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; +import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks"; import NoteContext from "../components/note_context"; import FAttribute from "../entities/fattribute"; import FNote from "../entities/fnote"; import { Attribute } from "../services/attribute_parser"; import attributes from "../services/attributes"; -import debounce from "../services/debounce"; import { t } from "../services/i18n"; import { DefinitionObject, extractAttributeDefinitionTypeAndName, LabelType } from "../services/promoted_attribute_definition_parser"; import server from "../services/server"; @@ -38,7 +37,7 @@ interface CellProps { } type OnChangeEventData = TargetedEvent | InputEvent | JQuery.TriggeredEvent; -type OnChangeListener = (e: OnChangeEventData) => Promise; +type OnChangeListener = (e: OnChangeEventData) => void | Promise; export default function PromotedAttributes() { const { note, componentId, noteContext } = useNoteContext(); @@ -110,7 +109,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone valueAttrs = valueAttrs.slice(0, 1); } - for (const [ i, valueAttr ] of valueAttrs.entries()) { + for (const valueAttr of valueAttrs) { const definition = definitionAttr.getDefinition(); // if not owned, we'll force creation of a new attribute instead of updating the inherited one @@ -183,19 +182,34 @@ const LABEL_MAPPINGS: Record = { url: "url" }; -function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) { - const { valueName, valueAttr, definition, definitionAttr } = props.cell; - const onChangeListener = buildPromotedAttributeLabelChangedListener({...props}); +function LabelInput(props: CellProps & { inputId: string }) { + const { inputId, note, cell, componentId, setCells } = props; + const { valueName, valueAttr, definition, definitionAttr } = cell; + const [ valueDraft, setDraft ] = useState(valueAttr.value); + const onChangeListener = useCallback(async (e: OnChangeEventData) => { + const inputEl = e.target as HTMLInputElement; + let value: string; + + if (inputEl.type === "checkbox") { + value = inputEl.checked ? "true" : "false"; + } else { + value = inputEl.value; + } + + await updateAttribute(note, cell, componentId, value, setCells); + }, [ cell, componentId, note, setCells ]); const extraInputProps: InputHTMLAttributes = {}; - useEffect(() => { - if (definition.labelType === "text") { - const el = document.getElementById(inputId); - if (el) { - setupTextLabelAutocomplete(el as HTMLInputElement, valueAttr, onChangeListener); - } + useTextLabelAutocomplete(inputId, valueAttr, definition, (e) => { + if (e.currentTarget instanceof HTMLInputElement) { + setDraft(e.currentTarget.value); } - }, [ inputId, valueAttr, onChangeListener ]); + }); + + // React to model changes. + useEffect(() => { + setDraft(valueAttr.value); + }, [ valueAttr.value ]); switch (definition.labelType) { case "number": { @@ -217,13 +231,13 @@ function LabelInput({ inputId, ...props }: CellProps & { inputId: string }) { tabIndex={200 + definitionAttr.position} id={inputId} type={LABEL_MAPPINGS[definition.labelType ?? "text"]} - value={valueAttr.value} + value={valueDraft} checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined} placeholder={t("promoted_attributes.unset-field-placeholder")} data-attribute-id={valueAttr.attributeId} data-attribute-type={valueAttr.type} data-attribute-name={valueAttr.name} - onChange={onChangeListener} + onBlur={onChangeListener} {...extraInputProps} />; @@ -399,16 +413,27 @@ function InputButton({ icon, className, title, onClick }: { ); } -function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, onChangeListener: OnChangeListener) { - // no need to await for this, can be done asynchronously - const $input = $(el); - server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributeValues) => { - if (_attributeValues.length === 0) { +function useTextLabelAutocomplete(inputId: string, valueAttr: Attribute, definition: DefinitionObject, onChangeListener: OnChangeListener) { + const [ attributeValues, setAttributeValues ] = useState<{ value: string }[] | null>(null); + + // Obtain data. + useEffect(() => { + if (definition.labelType !== "text") { return; } - const attributeValues = _attributeValues.map((attribute) => ({ value: attribute })); + server.get(`attribute-values/${encodeURIComponent(valueAttr.name)}`).then((_attributesValues) => { + setAttributeValues(_attributesValues.map((attribute) => ({ value: attribute }))); + }); + }, [ definition.labelType, valueAttr.name ]); + // Initialize autocomplete. + useEffect(() => { + if (attributeValues?.length === 0) return; + const el = document.getElementById(inputId) as HTMLInputElement | null; + if (!el) return; + + const $input = $(el); $input.autocomplete( { appendTo: document.querySelector("body"), @@ -424,7 +449,7 @@ function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, source (term, cb) { term = term.toLowerCase(); - const filtered = attributeValues.filter((attr) => attr.value.toLowerCase().includes(term)); + const filtered = (attributeValues ?? []).filter((attr) => attr.value.toLowerCase().includes(term)); cb(filtered); } @@ -434,27 +459,13 @@ function setupTextLabelAutocomplete(el: HTMLInputElement, valueAttr: Attribute, $input.off("autocomplete:selected"); $input.on("autocomplete:selected", onChangeListener); - }); -} -function buildPromotedAttributeLabelChangedListener({ note, cell, componentId, setCells }: CellProps): OnChangeListener { - async function onChange(e: OnChangeEventData) { - const inputEl = e.target as HTMLInputElement; - let value: string; - - if (inputEl.type === "checkbox") { - value = inputEl.checked ? "true" : "false"; - } else { - value = inputEl.value; - } - - await updateAttribute(note, cell, componentId, value, setCells); - } - - return debounce(onChange, 250); + return () => $input.autocomplete("destroy"); + }, [ inputId, attributeValues, onChangeListener ]); } async function updateAttribute(note: FNote, cell: Cell, componentId: string, value: string | undefined, setCells: Dispatch>) { + if (value === cell.valueAttr.value) return; const { attributeId } = await server.put( `notes/${note.noteId}/attribute`, { diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 9db0393692..25f48c5db2 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -47,6 +47,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: > {isMobile() && <> + } diff --git a/apps/client/src/widgets/collections/NoteList.css b/apps/client/src/widgets/collections/NoteList.css index 449f45d97c..d312f6a427 100644 --- a/apps/client/src/widgets/collections/NoteList.css +++ b/apps/client/src/widgets/collections/NoteList.css @@ -2,8 +2,12 @@ min-height: 0; max-width: var(--max-content-width); /* Inherited from .note-split */ - overflow: auto; + overflow: visible; contain: none !important; + + &.full-height { + overflow: auto; + } } body.prefers-centered-content .note-list-widget:not(.full-height) { @@ -19,14 +23,3 @@ body.prefers-centered-content .note-list-widget:not(.full-height) { .note-list-widget video { height: 100%; } - -/* #region Pagination */ -.note-list-pager { - font-size: 1rem; - - span.current-page { - text-decoration: underline; - font-weight: bold; - } -} -/* #endregion */ diff --git a/apps/client/src/widgets/collections/Pagination.css b/apps/client/src/widgets/collections/Pagination.css new file mode 100644 index 0000000000..93ee13a76d --- /dev/null +++ b/apps/client/src/widgets/collections/Pagination.css @@ -0,0 +1,88 @@ +:where(.note-list-pager) { + --note-list-pager-page-button-width: 40px; + --note-list-pager-page-button-gap: 3px; + --note-list-pager-ellipsis-width: 20px; + --note-list-pager-justify-content: flex-end; + + --note-list-pager-current-page-button-background-color: var(--button-group-active-button-background); + --note-list-pager-current-page-button-text-color: var(--button-group-active-button-text-color); +} + +.note-list-pager-container { + display: flex; + flex-direction: column; + width: 100%; + container: note-list-pager / inline-size; +} + +.note-list-pager { + display: flex; + align-items: center; + font-size: .8rem; + align-self: var(--note-list-pager-justify-content); + + .note-list-pager-nav-button { + --icon-button-icon-ratio: .75; + } + + .note-list-pager-page-button-container { + display: flex; + align-items: baseline; + justify-content: space-around; + gap: var(--note-list-pager-page-button-gap); + + &.note-list-pager-ellipsis-present { + /* Prevent the prev/next buttons from shifting when ellipses appear or disappear */ + --_gap-max-width: calc((var(--note-list-pager-page-button-count) + 2) * var(--note-list-pager-page-button-gap)); + + min-width: calc(var(--note-list-pager-page-button-count) * var(--note-list-pager-page-button-width) + + (var(--note-list-pager-ellipsis-width) * 2) + + var(--_gap-max-width)); + } + + .note-list-pager-page-button { + min-width: var(--note-list-pager-page-button-width); + padding-inline: 0; + padding-block: 4px; + + &.note-list-pager-page-button-current { + background: var(--note-list-pager-current-page-button-background-color); + color: var(--note-list-pager-current-page-button-text-color); + font-weight: bold; + opacity: unset; + } + } + + .note-list-pager-ellipsis { + display: inline-block; + width: var(--note-list-pager-ellipsis-width); + text-align: center; + opacity: .5; + } + } + + .note-list-pager-narrow-counter { + display: none; + min-width: 60px; + text-align: center; + white-space: nowrap; + } + + .note-list-pager-total-count { + margin-inline-start: 8px; + opacity: .5; + white-space: nowrap; + } + + @container note-list-pager (max-width: 550px) { + .note-list-pager-page-button-container, + .note-list-pager-total-count { + display: none; + } + + .note-list-pager-narrow-counter { + display: block; + } + } +} + diff --git a/apps/client/src/widgets/collections/Pagination.tsx b/apps/client/src/widgets/collections/Pagination.tsx index 6b74964a64..26d22215cb 100644 --- a/apps/client/src/widgets/collections/Pagination.tsx +++ b/apps/client/src/widgets/collections/Pagination.tsx @@ -4,6 +4,10 @@ import FNote from "../../entities/fnote"; import froca from "../../services/froca"; import { useNoteLabelInt } from "../react/hooks"; import { t } from "../../services/i18n"; +import ActionButton from "../react/ActionButton"; +import Button from "../react/Button"; +import "./Pagination.css"; +import clsx from "clsx"; interface PaginationContext { page: number; @@ -17,46 +21,106 @@ interface PaginationContext { export function Pager({ page, pageSize, setPage, pageCount, totalNotes }: Omit) { if (pageCount < 2) return; - let lastPrinted = false; - let children: ComponentChildren[] = []; - for (let i = 1; i <= pageCount; i++) { - if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(page - i) <= 2) { - lastPrinted = true; - - const startIndex = (i - 1) * pageSize + 1; - const endIndex = Math.min(totalNotes, i * pageSize); - - if (i !== page) { - children.push(( - setPage(i)} - > - {i} - - )) - } else { - // Current page - children.push({i}) - } - - children.push(<>{" "} {" "}); - } else if (lastPrinted) { - children.push(<>{"... "} {" "}); - lastPrinted = false; - } - } - return ( -
- {children} +
+
+ setPage(page - 1)} + /> - ({t("pagination.total_notes", { count: totalNotes })}) + +
+ {page} / {pageCount} +
+ + setPage(page + 1)} + /> + +
+ {t("pagination.total_notes", { count: totalNotes })} +
+
) } +interface PageButtonsProps { + page: number; + setPage: Dispatch>; + pageCount: number; +} + +function PageButtons(props: PageButtonsProps) { + const maxButtonCount = 9; + const maxLeftRightSegmentLength = 2; + + // The left-side segment + const leftLength = Math.min(props.pageCount, maxLeftRightSegmentLength); + const leftStart = 1; + + // The middle segment + const middleMaxLength = maxButtonCount - maxLeftRightSegmentLength * 2; + const middleLength = Math.min(props.pageCount - leftLength, middleMaxLength); + let middleStart = props.page - Math.floor(middleLength / 2); + middleStart = Math.max(middleStart, leftLength + 1); + + // The right-side segment + const rightLength = Math.min(props.pageCount - (middleLength + leftLength), maxLeftRightSegmentLength); + const rightStart = props.pageCount - rightLength + 1; + middleStart = Math.min(middleStart, rightStart - middleLength); + + const totalButtonCount = leftLength + middleLength + rightLength; + const hasLeadingEllipsis = (middleStart - leftLength > 1); + const hasTrailingEllipsis = (rightStart - (middleStart + middleLength - 1) > 1); + + return
+ {[ + ...createSegment(leftStart, leftLength, props.page, props.setPage, false), + ...createSegment(middleStart, middleLength, props.page, props.setPage, hasLeadingEllipsis), + ...createSegment(rightStart, rightLength, props.page, props.setPage, hasTrailingEllipsis), + ]} +
; +} + +function createSegment(start: number, length: number, currentPage: number, setPage: Dispatch>, prependEllipsis: boolean): ComponentChildren[] { + const children: ComponentChildren[] = []; + + if (prependEllipsis) { + children.push(...); + } + + for (let i = 0; i < length; i++) { + const pageNum = start + i; + const isCurrent = (pageNum === currentPage); + children.push(( +
); } +function useLayerData(note: FNote) { + const [ layerName ] = useNoteLabel(note, "map:style"); + // Memo is needed because it would generate unnecessary reloads due to layer change. + const layerData = useMemo(() => { + // Custom layers. + if (layerName?.startsWith("http")) { + return { + name: "Custom", + type: "raster", + url: layerName, + attribution: "" + } satisfies MapLayer; + } + + // Built-in layers. + const layerData = MAP_LAYERS[layerName ?? ""] ?? MAP_LAYERS[DEFAULT_MAP_LAYER_NAME]; + return layerData; + }, [ layerName ]); + + return layerData; +} + function ToggleReadOnlyButton({ note }: { note: FNote }) { const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly"); @@ -179,22 +202,26 @@ function ToggleReadOnlyButton({ note }: { note: FNote }) { />; } -function NoteWrapper({ note, isReadOnly }: { note: FNote, isReadOnly: boolean }) { +function NoteWrapper({ note, isReadOnly, hideLabels }: { + note: FNote, + isReadOnly: boolean, + hideLabels: boolean +}) { const mime = useNoteProperty(note, "mime"); const [ location ] = useNoteLabel(note, LOCATION_ATTRIBUTE); if (mime === "application/gpx+xml") { - return ; + return ; } if (location) { const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined; if (!latLng) return; - return ; + return ; } } -function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean, latLng: [number, number] }) { +function NoteMarker({ note, editable, latLng, hideLabels }: { note: FNote, editable: boolean, latLng: [number, number], hideLabels: boolean }) { // React to changes const [ color ] = useNoteLabel(note, "color"); const [ iconClass ] = useNoteLabel(note, "iconClass"); @@ -202,8 +229,9 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean const title = useNoteProperty(note, "title"); const icon = useMemo(() => { - return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, title, note.noteId, archived); - }, [ iconClass, color, title, note.noteId, archived]); + const titleOrNone = hideLabels ? undefined : title; + return buildIcon(note.getIcon(), note.getColorClass() ?? undefined, titleOrNone, note.noteId, archived); + }, [ iconClass, color, title, note.noteId, archived, hideLabels ]); const onClick = useCallback(() => { appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId }); @@ -235,7 +263,7 @@ function NoteMarker({ note, editable, latLng }: { note: FNote, editable: boolean />; } -function NoteGpxTrack({ note }: { note: FNote }) { +function NoteGpxTrack({ note, hideLabels }: { note: FNote, hideLabels?: boolean }) { const [ xmlString, setXmlString ] = useState(); const blob = useNoteBlob(note); @@ -256,7 +284,7 @@ function NoteGpxTrack({ note }: { note: FNote }) { const options = useMemo(() => ({ markers: { - startIcon: buildIcon(note.getIcon(), note.getColorClass(), note.title), + startIcon: buildIcon(note.getIcon(), note.getColorClass(), hideLabels ? undefined : note.title), endIcon: buildIcon("bxs-flag-checkered"), wptIcons: { "": buildIcon("bx bx-pin") @@ -265,7 +293,7 @@ function NoteGpxTrack({ note }: { note: FNote }) { polyline_options: { color: note.getLabelValue("color") ?? "blue" } - }), [ color, iconClass ]); + }), [ color, iconClass, hideLabels ]); return xmlString && ; } diff --git a/apps/client/src/widgets/collections/geomap/map.tsx b/apps/client/src/widgets/collections/geomap/map.tsx index 035f863cc8..19a4586c83 100644 --- a/apps/client/src/widgets/collections/geomap/map.tsx +++ b/apps/client/src/widgets/collections/geomap/map.tsx @@ -1,7 +1,7 @@ import { useEffect, useImperativeHandle, useRef, useState } from "preact/hooks"; import L, { control, LatLng, Layer, LeafletMouseEvent } from "leaflet"; import "leaflet/dist/leaflet.css"; -import { MAP_LAYERS } from "./map_layer"; +import { MAP_LAYERS, type MapLayer } from "./map_layer"; import { ComponentChildren, createContext, RefObject } from "preact"; import { useElementSize, useSyncedRef } from "../../react/hooks"; @@ -12,7 +12,7 @@ interface MapProps { containerRef?: RefObject; coordinates: LatLng | [number, number]; zoom: number; - layerName: string; + layerData: MapLayer; viewportChanged: (coordinates: LatLng, zoom: number) => void; children: ComponentChildren; onClick?: (e: LeafletMouseEvent) => void; @@ -21,7 +21,7 @@ interface MapProps { scale: boolean; } -export default function Map({ coordinates, zoom, layerName, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) { +export default function Map({ coordinates, zoom, layerData, viewportChanged, children, onClick, onContextMenu, scale, apiRef, containerRef: _containerRef, onZoom }: MapProps) { const mapRef = useRef(null); const containerRef = useSyncedRef(_containerRef); @@ -49,8 +49,6 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi const [ layer, setLayer ] = useState(); useEffect(() => { async function load() { - const layerData = MAP_LAYERS[layerName]; - if (layerData.type === "vector") { const style = (typeof layerData.style === "string" ? layerData.style : await layerData.style()); await import("@maplibre/maplibre-gl-leaflet"); @@ -68,7 +66,7 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi } load(); - }, [ layerName ]); + }, [ layerData ]); // Attach layer to the map. useEffect(() => { @@ -139,7 +137,7 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi return (
{children} diff --git a/apps/client/src/widgets/collections/geomap/map_layer.ts b/apps/client/src/widgets/collections/geomap/map_layer.ts index 7b12a10761..bb5f6174e6 100644 --- a/apps/client/src/widgets/collections/geomap/map_layer.ts +++ b/apps/client/src/widgets/collections/geomap/map_layer.ts @@ -1,20 +1,17 @@ -export interface MapLayer { - name: string; - isDarkTheme?: boolean; -} - -interface VectorLayer extends MapLayer { +export type MapLayer = ({ type: "vector"; style: string | (() => Promise<{}>) -} - -interface RasterLayer extends MapLayer { +} | { type: "raster"; url: string; attribution: string; -} +}) & { + // Common properties + name: string; + isDarkTheme?: boolean; +}; -export const MAP_LAYERS: Record = { +export const MAP_LAYERS: Record = { "openstreetmap": { name: "OpenStreetMap", type: "raster", diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.css b/apps/client/src/widgets/collections/legacy/ListOrGridView.css index 1bfa389e5c..222682cc9f 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.css +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.css @@ -1,5 +1,5 @@ .note-list { - overflow: hidden; + overflow: visible; position: relative; height: 100%; } @@ -100,23 +100,206 @@ overflow: auto; } -.note-expander { - font-size: x-large; - position: relative; - top: 3px; - cursor: pointer; +/* #region List view */ + +@keyframes note-preview-show { + from { + opacity: 0; + } to { + opacity: 1; + } } -.note-list-pager { - text-align: center; +.nested-note-list { + --card-nested-section-indent: 25px; + + &.search-results { + --card-nested-section-indent: 32px; + } } -.note-list.list-view .note-path { - margin-left: 0.5em; - vertical-align: middle; - opacity: 0.5; +/* List item */ +.nested-note-list-item { + h5 { + display: flex; + align-items: center; + font-size: 1em; + font-weight: normal; + margin: 0; + } + + .note-expander { + margin-inline-end: 4px; + font-size: x-large; + cursor: pointer; + } + + .tn-icon { + margin-inline-end: 8px; + color: var(--note-list-view-icon-color); + font-size: 1.2em; + } + + .note-book-title { + --link-hover-background: transparent; + --link-hover-color: currentColor; + color: inherit; + font-weight: normal; + } + + .note-path { + margin-left: 0.5em; + vertical-align: middle; + opacity: 0.5; + } + + .note-list-attributes { + flex-grow: 1; + margin-inline-start: 1em; + text-align: right; + font-size: .75em; + opacity: .75; + } + + .nested-note-list-item-menu { + margin-inline-start: 8px; + flex-shrink: 0; + } + + &.archived { + span.tn-icon + span, + .tn-icon { + opacity: .6; + } + } + + &.use-note-color { + span.tn-icon + span, + .nested-note-list:not(.search-results) & .tn-icon, + .rendered-note-attributes { + color: var(--custom-color); + } + } } +.nested-note-list:not(.search-results) h5 { + span.tn-icon + span, + .note-list-attributes { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +/* List item (search results view) */ +.nested-note-list.search-results .nested-note-list-item { + span.tn-icon + span > span { + display: flex; + flex-direction: column-reverse; + align-items: flex-start; + } + + small { + line-height: .85em; + } + + .note-path { + margin-left: 0; + font-size: .85em; + line-height: .85em; + font-weight: 500; + letter-spacing: .5pt; + } + + .tn-icon { + display: flex; + flex-shrink: 0; + justify-content: center; + align-items: center; + width: 1.75em; + height: 1.75em; + margin-inline-end: 12px; + border-radius: 50%; + background: var(--note-icon-custom-background-color, var(--note-list-view-large-icon-background)); + font-size: 1.2em; + color: var(--note-icon-custom-color, var(--note-list-view-large-icon-color)); + } + + h5 .ck-find-result { + background: var(--note-list-view-search-result-highlight-background); + color: var(--note-list-view-search-result-highlight-color); + font-weight: 600; + text-decoration: underline; + } +} + +/* Note content preview */ +.nested-note-list .note-book-content { + display: none; + outline: 1px solid var(--note-list-view-content-background); + border-radius: 8px; + background-color: var(--note-list-view-content-background); + overflow: hidden; + user-select: text; + font-size: .85rem; + animation: note-preview-show .25s ease-out; + will-change: opacity; + + &.note-book-content-ready { + display: block; + } + + > .rendered-content > *:last-child { + margin-bottom: 0; + } + + &.type-text { + padding: 8px 24px; + + .ck-content > *:last-child { + margin-bottom: 0; + } + } + + &.type-protectedSession { + padding: 20px; + } + + &.type-image { + padding: 0; + } + + &.type-pdf { + iframe { + height: 50vh; + } + + .file-footer { + padding: 8px; + } + } + + &.type-webView { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 50vh; + } + + .ck-find-result { + outline: 2px solid var(--note-list-view-content-search-result-highlight-background); + border-radius: 4px; + background: var(--note-list-view-content-search-result-highlight-background); + color: var(--note-list-view-content-search-result-highlight-color); + } +} + +.note-content-preview:has(.note-book-content:empty) { + display: none; +} + +/* #endregion */ + /* #region Grid view */ .note-list.grid-view .note-list-container { display: flex; @@ -128,6 +311,10 @@ border: 1px solid transparent; } +body.mobile .note-list.grid-view .note-book-card { + flex-basis: 150px; +} + .note-list.grid-view .note-book-card { max-height: 300px; } diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx index 61c7193a6e..cdde8a6087 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx @@ -1,4 +1,5 @@ import "./ListOrGridView.css"; +import { Card, CardSection } from "../../react/Card"; import { useEffect, useRef, useState } from "preact/hooks"; @@ -14,6 +15,11 @@ import NoteLink from "../../react/NoteLink"; import { ViewModeProps } from "../interface"; import { Pager, usePagination } from "../Pagination"; import { filterChildNotes, useFilteredNoteIds } from "./utils"; +import { JSX } from "preact/jsx-runtime"; +import { clsx } from "clsx"; +import ActionButton from "../../react/ActionButton"; +import linkContextMenuService from "../../../menus/link_context_menu"; +import { TargetedMouseEvent } from "preact"; export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) { const expandDepth = useExpansionDepth(note); @@ -33,7 +39,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens } { noteIds.length > 0 &&
{!hasCollectionProperties && } - +
} @@ -93,27 +99,52 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan // Reset expand state if switching to another note, or if user manually toggled expansion state. useEffect(() => setExpanded(currentLevel <= expandDepth), [ note, currentLevel, expandDepth ]); + let subSections: JSX.Element | undefined = undefined; + if (isExpanded) { + subSections = <> + + + + + + + } + return ( -
-
- setExpanded(!isExpanded)} - /> - +
+ setExpanded(!isExpanded)}/> - + + openNoteMenu(notePath, e)} + />
- - {isExpanded && <> - - - } -
+ ); } @@ -165,6 +196,9 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc const contentRef = useRef(null); const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens); + const [ready, setReady] = useState(false); + const [noteType, setNoteType] = useState("none"); + useEffect(() => { content_renderer.getRenderedContent(note, { trim, @@ -179,17 +213,19 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc } else { contentRef.current.replaceChildren(); } - contentRef.current.classList.add(`type-${type}`); highlightSearch(contentRef.current); + setNoteType(type); + setReady(true); }) .catch(e => { console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`); console.error(e); contentRef.current?.replaceChildren(t("collections.rendering_error")); + setReady(true); }); }, [ note, highlightedTokens ]); - return
; + return
; } function NoteChildren({ note, parentNote, highlightedTokens, currentLevel, expandDepth, includeArchived }: { @@ -238,3 +274,8 @@ function useExpansionDepth(note: FNote) { return parseInt(expandDepth, 10); } + +function openNoteMenu(notePath, e: TargetedMouseEvent) { + linkContextMenuService.openContextMenu(notePath, e); + e.stopPropagation() +} diff --git a/apps/client/src/widgets/containers/scrolling_container.css b/apps/client/src/widgets/containers/scrolling_container.css index 0e590801c4..a3fae557a3 100644 --- a/apps/client/src/widgets/containers/scrolling_container.css +++ b/apps/client/src/widgets/containers/scrolling_container.css @@ -19,6 +19,10 @@ } } +body.mobile .scrolling-container { + --content-margin-inline: 8px; +} + .note-split.type-code:not(.mime-text-x-sqlite) { &> .scrolling-container { background-color: var(--code-background-color); diff --git a/apps/client/src/widgets/dialogs/PopupEditor.css b/apps/client/src/widgets/dialogs/PopupEditor.css index 3357b00f3e..84f4c93197 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.css +++ b/apps/client/src/widgets/dialogs/PopupEditor.css @@ -32,6 +32,12 @@ body.mobile .modal.popup-editor-dialog .modal-dialog { display: flex; align-items: center; margin-block: 0; + + .note-icon-widget { + display: flex; + align-items: center; + margin-inline-start: 0; + } } .modal.popup-editor-dialog .modal-header .note-title-widget { diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index a7f3fde393..07363b2e91 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -67,10 +67,7 @@ export default function PopupEditor() { - - {isNewLayout && } - } + title={} customTitleBarButtons={[{ iconClassName: "bx-expand-alt", title: t("popup-editor.maximize"), @@ -123,6 +120,7 @@ export function TitleRow() {
+ {isNewLayout && }
); } diff --git a/apps/client/src/widgets/dialogs/bulk_actions.tsx b/apps/client/src/widgets/dialogs/bulk_actions.tsx index 05033255c4..cbb04a404a 100644 --- a/apps/client/src/widgets/dialogs/bulk_actions.tsx +++ b/apps/client/src/widgets/dialogs/bulk_actions.tsx @@ -57,7 +57,7 @@ export default function BulkActionsDialog() { className="bulk-actions-dialog" size="xl" title={t("bulk_actions.bulk_actions")} - footer={
); } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index c608c8f203..16f7ad1123 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -414,7 +414,7 @@ function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) { const dropdownRef = useRef(null); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const count = sortedNotePaths?.length ?? 0; - const enabled = count > 1; + const enabled = true; // Keyboard shortcut. useTriliumEvent("toggleRibbonTabNotePaths", () => enabled && dropdownRef.current?.show()); diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 8be65240b2..58b6c1fbc3 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -35,11 +35,6 @@ display: flex; flex-direction: column; - &.with-hue { - background-color: hsl(var(--bg-hue), 8.8%, 11.2%); - border-color: hsl(var(--bg-hue), 9.4%, 25.1%); - } - &.active { outline: 4px solid var(--more-accented-background-color); background: var(--card-background-hover-color); diff --git a/apps/client/src/widgets/mobile_widgets/sidebar_container.ts b/apps/client/src/widgets/mobile_widgets/sidebar_container.ts index ef719d36a9..f79d8a72c2 100644 --- a/apps/client/src/widgets/mobile_widgets/sidebar_container.ts +++ b/apps/client/src/widgets/mobile_widgets/sidebar_container.ts @@ -13,7 +13,7 @@ const DRAG_OPEN_THRESHOLD = 10; /** The number of pixels the user has to drag across the screen to the right when the sidebar is closed to trigger the drag open animation. */ const DRAG_CLOSED_START_THRESHOLD = 10; /** The number of pixels the user has to drag across the screen to the left when the sidebar is opened to trigger the drag close animation. */ -const DRAG_OPENED_START_THRESHOLD = 80; +const DRAG_OPENED_START_THRESHOLD = 100; export default class SidebarContainer extends FlexContainer { private screenName: Screen; @@ -54,7 +54,7 @@ export default class SidebarContainer extends FlexContainer { this.startX = x; // Prevent dragging if too far from the edge of the screen and the menu is closed. - let dragRefX = glob.isRtl ? this.screenWidth - x : x; + const dragRefX = glob.isRtl ? this.screenWidth - x : x; if (dragRefX > 30 && this.currentTranslate === -100) { return; } @@ -89,7 +89,7 @@ export default class SidebarContainer extends FlexContainer { } } else if (this.dragState === DRAG_STATE_DRAGGING) { const width = this.sidebarEl.offsetWidth; - let translatePercentage = Math.min(0, Math.max(this.currentTranslate + (deltaX / width) * 100, -100)); + const translatePercentage = Math.min(0, Math.max(this.currentTranslate + (deltaX / width) * 100, -100)); const backdropOpacity = Math.max(0, 1 + translatePercentage / 100); this.translatePercentage = translatePercentage; if (glob.isRtl) { @@ -160,12 +160,10 @@ export default class SidebarContainer extends FlexContainer { this.sidebarEl.classList.toggle("show", isOpen); if (isOpen) { this.sidebarEl.style.transform = "translateX(0)"; + } else if (glob.isRtl) { + this.sidebarEl.style.transform = "translateX(100%)"; } else { - if (glob.isRtl) { - this.sidebarEl.style.transform = "translateX(100%)" - } else { - this.sidebarEl.style.transform = "translateX(-100%)"; - } + this.sidebarEl.style.transform = "translateX(-100%)"; } this.sidebarEl.style.transition = this.originalSidebarTransition; diff --git a/apps/client/src/widgets/mobile_widgets/toggle_sidebar_button.tsx b/apps/client/src/widgets/mobile_widgets/toggle_sidebar_button.tsx index 8e689954b0..b87e8cb90e 100644 --- a/apps/client/src/widgets/mobile_widgets/toggle_sidebar_button.tsx +++ b/apps/client/src/widgets/mobile_widgets/toggle_sidebar_button.tsx @@ -1,5 +1,5 @@ -import ActionButton from "../react/ActionButton"; import { t } from "../../services/i18n"; +import ActionButton from "../react/ActionButton"; import { useNoteContext } from "../react/hooks"; export default function ToggleSidebarButton() { @@ -10,10 +10,15 @@ export default function ToggleSidebarButton() { { noteContext?.isMainContext() && parentComponent?.triggerCommand("setActiveScreen", { - screen: "tree" - })} + onClick={(e) => { + // Remove focus to prevent tooltip showing on top of the sidebar. + (e.currentTarget as HTMLButtonElement).blur(); + + parentComponent?.triggerCommand("setActiveScreen", { + screen: "tree" + }); + }} />}
- ) + ); } diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.css b/apps/client/src/widgets/note_bars/CollectionProperties.css index 99700f77a7..c8802a42a0 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.css +++ b/apps/client/src/widgets/note_bars/CollectionProperties.css @@ -5,7 +5,7 @@ align-items: center; width: 100%; max-width: unset; - font-size: 0.8em; + font-size: 0.8rem; .dropdown-menu { input.form-control { diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.tsx b/apps/client/src/widgets/note_bars/CollectionProperties.tsx index 5dba675e6d..66bfb32c45 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.tsx +++ b/apps/client/src/widgets/note_bars/CollectionProperties.tsx @@ -2,18 +2,16 @@ import "./CollectionProperties.css"; import { t } from "i18next"; import { ComponentChildren } from "preact"; -import { useContext, useRef } from "preact/hooks"; -import { Fragment } from "preact/jsx-runtime"; +import { useRef } from "preact/hooks"; import FNote from "../../entities/fnote"; import { ViewTypeOptions } from "../collections/interface"; import Dropdown from "../react/Dropdown"; -import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; -import FormTextBox from "../react/FormTextBox"; -import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { useNoteProperty, useTriliumEvent } from "../react/hooks"; import Icon from "../react/Icon"; -import { ParentComponent } from "../react/react_utils"; -import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; +import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu"; +import { bookPropertiesConfig } from "../ribbon/collection-properties-config"; import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab"; export const ICON_MAPPINGS: Record = { @@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption - {properties.map(property => ( - + {properties.map((property, index) => ( + ))} {properties.length > 0 && } @@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption ); } - -function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) { - switch (property.type) { - case "button": - return ; - case "split-button": - return ; - case "checkbox": - return ; - case "number": - return ; - case "combobox": - return ; - } -} - -function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { - const parentComponent = useContext(ParentComponent); - - return ( - { - if (!parentComponent) return; - property.onClick({ - note, - triggerCommand: parentComponent.triggerCommand.bind(parentComponent) - }); - }} - >{property.label} - ); -} - -function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) { - const parentComponent = useContext(ParentComponent); - const ItemsComponent = property.items; - const clickContext = parentComponent && { - note, - triggerCommand: parentComponent.triggerCommand.bind(parentComponent) - }; - - return (parentComponent && - clickContext && property.onClick(clickContext)} - > - - - ); -} - -function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) { - //@ts-expect-error Interop with text box which takes in string values even for numbers. - const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); - const disabled = property.disabled?.(note); - - return ( - e.stopPropagation()} - > - {property.label} - - - ); -} - -function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) { - const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? ""); - - function renderItem(option: ComboBoxItem) { - return ( - setValue(option.value)} - > - {option.label} - - ); - } - - return ( - - {(property.options).map((option, index) => { - if ("items" in option) { - return ( - - {option.title} - {option.items.map(renderItem)} - {index < property.options.length - 1 && } - - ); - } - return renderItem(option); - - })} - - ); -} - -function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) { - const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel); - return ( - - ); -} diff --git a/apps/client/src/widgets/note_icon.tsx b/apps/client/src/widgets/note_icon.tsx index 9df9ad48f4..31ca7e65b4 100644 --- a/apps/client/src/widgets/note_icon.tsx +++ b/apps/client/src/widgets/note_icon.tsx @@ -69,7 +69,7 @@ function MobileNoteIconSwitcher({ note, icon }: { const [ modalShown, setModalShown ] = useState(false); const { windowWidth } = useWindowSize(); - return (note && + return (
- setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} /> + {note && setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />} ), document.body)}
diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index feb5972ef4..764e155c44 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; import keyboard_actions from "../../services/keyboard_actions"; +import { isMobile } from "../../services/utils"; import { useStaticTooltip } from "./hooks"; export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu" | "style"> { @@ -17,6 +18,8 @@ export interface ActionButtonProps extends Pick(null); const [ keyboardShortcut, setKeyboardShortcut ] = useState(); @@ -25,6 +28,7 @@ export default function ActionButton({ text, icon, className, triggerCommand, ti title: keyboardShortcut?.length ? `${text} (${keyboardShortcut?.join(",")})` : text, placement: titlePosition ?? "bottom", fallbackPlacements: [ titlePosition ?? "bottom" ], + trigger: cachedIsMobile ? "focus" : "hover focus", animation: false }); diff --git a/apps/client/src/widgets/react/Button.tsx b/apps/client/src/widgets/react/Button.tsx index ffdc6ddd2c..6269000563 100644 --- a/apps/client/src/widgets/react/Button.tsx +++ b/apps/client/src/widgets/react/Button.tsx @@ -1,13 +1,14 @@ -import type { ComponentChildren, RefObject } from "preact"; -import type { CSSProperties } from "preact/compat"; +import type { ComponentChildren, CSSProperties, RefObject } from "preact"; import { memo } from "preact/compat"; import { useMemo } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; -import { isDesktop } from "../../services/utils"; +import { isDesktop, isMobile } from "../../services/utils"; import ActionButton from "./ActionButton"; import Icon from "./Icon"; +const cachedIsMobile = isMobile(); + export interface ButtonProps { name?: string; /** Reference to the button element. Mostly useful for requesting focus. */ @@ -18,7 +19,7 @@ export interface ButtonProps { keyboardShortcut?: string; /** Called when the button is clicked. If not set, the button will submit the form (if any). */ onClick?: () => void; - primary?: boolean; + kind?: "primary" | "secondary" | "lowProfile"; disabled?: boolean; size?: "normal" | "small" | "micro"; style?: CSSProperties; @@ -26,15 +27,23 @@ export interface ButtonProps { title?: string; } -const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => { +const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortcut, icon, kind, disabled, size, style, triggerCommand, ...restProps }: ButtonProps) => { // Memoize classes array to prevent recreation const classes = useMemo(() => { const classList: string[] = ["btn"]; - if (primary) { - classList.push("btn-primary"); - } else { - classList.push("btn-secondary"); + + switch(kind) { + case "primary": + classList.push("btn-primary"); + break; + case "lowProfile": + classList.push("tn-low-profile"); + break; + default: + classList.push("btn-secondary"); + break; } + if (className) { classList.push(className); } @@ -44,11 +53,11 @@ const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortc classList.push("btn-micro"); } return classList.join(" "); - }, [primary, className, size]); + }, [kind, className, size]); // Memoize keyboard shortcut rendering const shortcutElements = useMemo(() => { - if (!keyboardShortcut) return null; + if (!keyboardShortcut || cachedIsMobile) return null; const splitShortcut = keyboardShortcut.split("+"); return splitShortcut.map((key, index) => ( <> diff --git a/apps/client/src/widgets/react/Card.css b/apps/client/src/widgets/react/Card.css new file mode 100644 index 0000000000..cabc4a0889 --- /dev/null +++ b/apps/client/src/widgets/react/Card.css @@ -0,0 +1,47 @@ +:where(.tn-card) { + --card-border-radius: 8px; + --card-padding-block: 8px; + --card-padding-inline: 16px; + --card-section-gap: 3px; + --card-nested-section-indent: 30px; +} + +.tn-card-heading { + margin-bottom: 10px; + font-size: .75rem; + font-weight: 600; + letter-spacing: .4pt; + text-transform: uppercase; +} + +.tn-card-body { + display: flex; + flex-direction: column; + gap: var(--card-section-gap); + + .tn-card-section { + padding: var(--card-padding-block) var(--card-padding-inline); + border: 1px solid var(--card-border-color, var(--main-border-color)); + background: var(--card-background-color); + + &:first-of-type { + border-top-left-radius: var(--card-border-radius); + border-top-right-radius: var(--card-border-radius); + } + + &:last-of-type { + border-bottom-left-radius: var(--card-border-radius); + border-bottom-right-radius: var(--card-border-radius); + } + + &.tn-card-section-nested { + padding-left: calc(var(--card-padding-inline) + var(--card-nested-section-indent) * var(--tn-card-section-nesting-level)); + background-color: color-mix(in srgb, var(--card-background-color) calc(100% / (var(--tn-card-section-nesting-level) + 1)) , transparent); + } + + &.tn-card-section-highlight-on-hover:hover { + background-color: var(--card-background-hover-color); + transition: background-color .2s ease-out; + } + } +} \ No newline at end of file diff --git a/apps/client/src/widgets/react/Card.tsx b/apps/client/src/widgets/react/Card.tsx new file mode 100644 index 0000000000..224a151504 --- /dev/null +++ b/apps/client/src/widgets/react/Card.tsx @@ -0,0 +1,63 @@ +import "./Card.css"; +import { ComponentChildren, createContext } from "preact"; +import { JSX } from "preact"; +import { useContext } from "preact/hooks"; +import clsx from "clsx"; + +// #region Card + +export interface CardProps { + className?: string; + heading?: string; +} + +export function Card(props: {children: ComponentChildren} & CardProps) { + return
+ {props.heading &&
{props.heading}
} +
+ {props.children} +
+
; +} + +// #endregion + +// #region Card Section + +export interface CardSectionProps { + className?: string; + subSections?: JSX.Element | JSX.Element[]; + subSectionsVisible?: boolean; + highlightOnHover?: boolean; + onAction?: () => void; +} + +interface CardSectionContextType { + nestingLevel: number; +} + +const CardSectionContext = createContext(undefined); + +export function CardSection(props: {children: ComponentChildren} & CardSectionProps) { + const parentContext = useContext(CardSectionContext); + const nestingLevel = (parentContext && parentContext.nestingLevel + 1) ?? 0; + + return <> +
0, + "tn-card-section-highlight-on-hover": props.highlightOnHover || props.onAction + })} + style={{"--tn-card-section-nesting-level": (nestingLevel) ? nestingLevel : null}} + onClick={props.onAction}> + {props.children} +
+ + {props.subSectionsVisible && props.subSections && + + {props.subSections} + + } + ; +} + +// #endregion \ No newline at end of file diff --git a/apps/client/src/widgets/react/NotePropertyMenu.tsx b/apps/client/src/widgets/react/NotePropertyMenu.tsx new file mode 100644 index 0000000000..72c7a12d86 --- /dev/null +++ b/apps/client/src/widgets/react/NotePropertyMenu.tsx @@ -0,0 +1,212 @@ +import { FilterLabelsByType } from "@triliumnext/commons"; +import { Fragment, VNode } from "preact"; +import { useContext } from "preact/hooks"; + +import Component from "../../components/component"; +import FNote from "../../entities/fnote"; +import NoteContextAwareWidget from "../note_context_aware_widget"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList"; +import FormTextBox from "./FormTextBox"; +import { useNoteLabel, useNoteLabelBoolean } from "./hooks"; +import { ParentComponent } from "./react_utils"; + +export interface ClickContext { + note: FNote; + triggerCommand: NoteContextAwareWidget["triggerCommand"]; +} + +export interface CheckBoxProperty { + type: "checkbox", + label: string; + bindToLabel: FilterLabelsByType; + icon?: string; + /** When true, the checkbox will be checked when the label value is false. Useful when the label represents a "hide" action, without exposing double negatives to the user. */ + reverseValue?: boolean; +} + +export interface ButtonProperty { + type: "button", + label: string; + title?: string; + icon?: string; + onClick(context: ClickContext): void; +} + +export interface SplitButtonProperty extends Omit { + type: "split-button"; + items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode; +} + +export interface NumberProperty { + type: "number", + label: string; + bindToLabel: FilterLabelsByType; + width?: number; + min?: number; + icon?: string; + disabled?: (note: FNote) => boolean; +} + +export interface ComboBoxItem { + /** + * The value to set to the bound label, `null` has a special meaning which removes the label entirely. + */ + value: string | null; + label: string; +} + +export interface ComboBoxGroup { + title: string; + items: ComboBoxItem[]; +} + +interface Separator { + type: "separator" +} + +export interface ComboBoxProperty { + type: "combobox", + label: string; + icon?: string; + bindToLabel: FilterLabelsByType; + /** + * The default value is used when the label is not set. + */ + defaultValue?: string; + options: (ComboBoxItem | Separator | ComboBoxGroup)[]; + dropStart?: boolean; +} + +export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator; + +export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) { + switch (property.type) { + case "button": + return ; + case "split-button": + return ; + case "checkbox": + return ; + case "number": + return ; + case "combobox": + return ; + case "separator": + return ; + } +} + +function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { + const parentComponent = useContext(ParentComponent); + + return ( + { + if (!parentComponent) return; + property.onClick({ + note, + triggerCommand: parentComponent.triggerCommand.bind(parentComponent) + }); + }} + >{property.label} + ); +} + +function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) { + const parentComponent = useContext(ParentComponent); + const ItemsComponent = property.items; + const clickContext = parentComponent && { + note, + triggerCommand: parentComponent.triggerCommand.bind(parentComponent) + }; + + return (parentComponent && + clickContext && property.onClick(clickContext)} + > + + + ); +} + +function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) { + //@ts-expect-error Interop with text box which takes in string values even for numbers. + const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); + const disabled = property.disabled?.(note); + + return ( + e.stopPropagation()} + > + {property.label} + + + ); +} + +function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) { + const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); + const valueWithDefault = value ?? property.defaultValue ?? null; + + function renderItem(option: ComboBoxItem) { + return ( + setValue(option.value)} + > + {option.label} + + ); + } + + return ( + + {(property.options).map((option, index) => { + if ("items" in option) { + return ( + + {option.title} + {option.items.map(renderItem)} + {index < property.options.length - 1 && } + + ); + } + if ("type" in option) { + return ; + } + + return renderItem(option); + + })} + + ); +} + +function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) { + const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel); + return ( + setValue(property.reverseValue ? !newValue : newValue)} + /> + ); +} diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 8eb2cc0efd..2dd5de4d5f 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -551,7 +551,12 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re useTriliumEvent("entitiesReloaded", ({ loadResults }) => { for (const attr of loadResults.getAttributeRows()) { if (attr.type === "relation" && attr.name === relationName && attributes.isAffecting(attr, note)) { - setRelationValue(attr.value ?? null); + if (!attr.isDeleted) { + setRelationValue(attr.value ?? null); + } else { + setRelationValue(null); + } + break; } } }); @@ -601,6 +606,7 @@ export function useNoteLabel(note: FNote | undefined | null, labelName: FilterLa } else { setLabelValue(null); } + break; } } }); @@ -1208,6 +1214,12 @@ export function useNoteTitle(noteId: string | undefined, parentNoteId: string | refresh(); }); + // React to external changes. + useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => { + if (loadResults.isNoteReloaded(noteId) || (parentNoteId && loadResults.getBranchRows().some(b => b.noteId === noteId && b.parentNoteId === parentNoteId))) { + refresh(); + } + }, [noteId, parentNoteId, refresh])); return title; } diff --git a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx index 4af8247a3f..e25f2650c9 100644 --- a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx @@ -1,18 +1,20 @@ -import { useContext, useMemo } from "preact/hooks"; -import { t } from "../../services/i18n"; -import FormSelect, { FormSelectWithGroups } from "../react/FormSelect"; -import { TabContext } from "./ribbon-interface"; -import { mapToKeyValueArray } from "../../services/utils"; -import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; -import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "./collection-properties-config"; -import Button, { SplitButton } from "../react/Button"; -import { ParentComponent } from "../react/react_utils"; -import FNote from "../../entities/fnote"; -import FormCheckbox from "../react/FormCheckbox"; -import FormTextBox from "../react/FormTextBox"; import { ComponentChildren } from "preact"; -import { ViewTypeOptions } from "../collections/interface"; +import { useContext, useMemo } from "preact/hooks"; + +import FNote from "../../entities/fnote"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; +import { t } from "../../services/i18n"; +import { mapToKeyValueArray } from "../../services/utils"; +import { ViewTypeOptions } from "../collections/interface"; +import Button, { SplitButton } from "../react/Button"; +import FormCheckbox from "../react/FormCheckbox"; +import FormSelect, { FormSelectWithGroups } from "../react/FormSelect"; +import FormTextBox from "../react/FormTextBox"; +import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; +import { BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxGroup, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../react/NotePropertyMenu"; +import { ParentComponent } from "../react/react_utils"; +import { bookPropertiesConfig } from "./collection-properties-config"; +import { TabContext } from "./ribbon-interface"; export const VIEW_TYPE_MAPPINGS: Record = { grid: t("book_properties.grid"), @@ -50,70 +52,70 @@ export function useViewType(note: FNote | null | undefined) { } function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) { - const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []); + const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []); - return ( -
- {t("book_properties.view_type")}:    - -
- ) + return ( +
+ {t("book_properties.view_type")}:    + +
+ ); } -function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) { - return ( - <> - {properties.map(property => ( -
- {mapPropertyView({ note, property })} -
- ))} +function BookProperties({ note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) { + return ( + <> + {properties.map((property, index) => ( +
+ {mapPropertyView({ note, property })} +
+ ))} - - - ) + + + ); } function mapPropertyView({ note, property }: { note: FNote, property: BookProperty }) { - switch (property.type) { - case "button": - return - case "split-button": - return - case "checkbox": - return - case "number": - return - case "combobox": - return - } + switch (property.type) { + case "button": + return ; + case "split-button": + return ; + case "checkbox": + return ; + case "number": + return ; + case "combobox": + return ; + } } function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { - const parentComponent = useContext(ParentComponent); + const parentComponent = useContext(ParentComponent); - return
- ) + ); } function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onClick: (e: MouseEvent) => void }) { const [ html, setHtml ] = useState | string>(""); useEffect(() => { attribute_renderer.renderAttribute(attribute, false).then(setHtml); - }, []); + }, [ attribute ]); return ( - @@ -230,15 +229,6 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) { />; } -function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) { - const isEnabled = noteMime.startsWith("application/javascript;env="); - return isEnabled && openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} - />; -} - function InAppHelpButton({ note }: NoteActionsCustomInnerProps) { const helpUrl = getHelpUrlForNote(note); const isEnabled = !!helpUrl; diff --git a/apps/client/src/widgets/ribbon/collection-properties-config.tsx b/apps/client/src/widgets/ribbon/collection-properties-config.tsx index 1f79217e9c..b4a46a89bc 100644 --- a/apps/client/src/widgets/ribbon/collection-properties-config.tsx +++ b/apps/client/src/widgets/ribbon/collection-properties-config.tsx @@ -1,79 +1,19 @@ import { t } from "i18next"; + +import Component from "../../components/component"; import FNote from "../../entities/fnote"; import attributes from "../../services/attributes"; -import NoteContextAwareWidget from "../note_context_aware_widget"; import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer"; import { ViewTypeOptions } from "../collections/interface"; -import { FilterLabelsByType } from "@triliumnext/commons"; import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes"; -import { VNode } from "preact"; -import { useNoteLabel } from "../react/hooks"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import Component from "../../components/component"; +import { useNoteLabel } from "../react/hooks"; +import { BookProperty, ClickContext, ComboBoxItem } from "../react/NotePropertyMenu"; interface BookConfig { properties: BookProperty[]; } -export interface CheckBoxProperty { - type: "checkbox", - label: string; - bindToLabel: FilterLabelsByType; - icon?: string; -} - -export interface ButtonProperty { - type: "button", - label: string; - title?: string; - icon?: string; - onClick(context: BookContext): void; -} - -export interface SplitButtonProperty extends Omit { - type: "split-button"; - items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode; -} - -export interface NumberProperty { - type: "number", - label: string; - bindToLabel: FilterLabelsByType; - width?: number; - min?: number; - icon?: string; - disabled?: (note: FNote) => boolean; -} - -export interface ComboBoxItem { - value: string; - label: string; -} - -interface ComboBoxGroup { - title: string; - items: ComboBoxItem[]; -} - -export interface ComboBoxProperty { - type: "combobox", - label: string; - icon?: string; - bindToLabel: FilterLabelsByType; - /** - * The default value is used when the label is not set. - */ - defaultValue?: string; - options: (ComboBoxItem | ComboBoxGroup)[]; -} - -export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty; - -interface BookContext { - note: FNote; - triggerCommand: NoteContextAwareWidget["triggerCommand"]; -} - export const bookPropertiesConfig: Record = { grid: { properties: [] @@ -156,6 +96,13 @@ export const bookPropertiesConfig: Record = { icon: "bx bx-ruler", type: "checkbox", bindToLabel: "map:scale" + }, + { + label: t("book_properties_config.show-labels"), + icon: "bx bx-label", + type: "checkbox", + bindToLabel: "map:hideLabels", + reverseValue: true } ] }, @@ -211,7 +158,7 @@ function ListExpandDepth(context: { note: FNote, parentComponent: Component }) { - ) + ); } function ListExpandDepthButton({ label, depth, note, parentComponent, checked }: { label: string, depth: number | "all", note: FNote, parentComponent: Component, checked?: boolean }) { @@ -226,7 +173,7 @@ function ListExpandDepthButton({ label, depth, note, parentComponent, checked }: } function buildExpandListHandler(depth: number | "all") { - return async ({ note, triggerCommand }: BookContext) => { + return async ({ note, triggerCommand }: ClickContext) => { const { noteId } = note; const existingValue = note.getLabelValue("expanded"); @@ -236,5 +183,5 @@ function buildExpandListHandler(depth: number | "all") { await attributes.setLabel(noteId, "expanded", newValue); triggerCommand("refreshNoteList", { noteId }); - } + }; } diff --git a/apps/client/src/widgets/tab_row.ts b/apps/client/src/widgets/tab_row.ts index b78952fafb..1089f49c9b 100644 --- a/apps/client/src/widgets/tab_row.ts +++ b/apps/client/src/widgets/tab_row.ts @@ -1,13 +1,14 @@ import Draggabilly, { type MoveVector } from "draggabilly"; -import { t } from "../services/i18n.js"; -import BasicWidget from "./basic_widget.js"; -import contextMenu from "../menus/context_menu.js"; -import utils from "../services/utils.js"; -import keyboardActionService from "../services/keyboard_actions.js"; -import appContext, { type CommandNames, type CommandListenerData, type EventData } from "../components/app_context.js"; -import froca from "../services/froca.js"; -import attributeService from "../services/attributes.js"; + +import appContext, { type CommandListenerData, type CommandNames, type EventData } from "../components/app_context.js"; import type NoteContext from "../components/note_context.js"; +import contextMenu from "../menus/context_menu.js"; +import attributeService from "../services/attributes.js"; +import froca from "../services/froca.js"; +import { t } from "../services/i18n.js"; +import keyboardActionService from "../services/keyboard_actions.js"; +import utils from "../services/utils.js"; +import BasicWidget from "./basic_widget.js"; import { setupHorizontalScrollViaWheel } from "./widget_utils.js"; const isDesktop = utils.isDesktop(); @@ -96,7 +97,6 @@ const TAB_ROW_TPL = ` .tab-row-filler { box-sizing: border-box; -webkit-app-region: drag; - height: 100%; min-width: ${MIN_FILLER_WIDTH}px; flex-grow: 1; } diff --git a/apps/client/src/widgets/type_widgets/Book.tsx b/apps/client/src/widgets/type_widgets/Book.tsx index 8dd1030c5d..05a1e2f058 100644 --- a/apps/client/src/widgets/type_widgets/Book.tsx +++ b/apps/client/src/widgets/type_widgets/Book.tsx @@ -1,11 +1,13 @@ -import { t } from "../../services/i18n"; -import Alert from "../react/Alert"; -import { useNoteLabelWithDefault, useTriliumEvent } from "../react/hooks"; -import RawHtml from "../react/RawHtml"; -import { TypeWidgetProps } from "./type_widget"; import "./Book.css"; + import { useEffect, useState } from "preact/hooks"; + +import { t } from "../../services/i18n"; import { ViewTypeOptions } from "../collections/interface"; +import CollectionProperties from "../note_bars/CollectionProperties"; +import { useNoteLabelWithDefault, useTriliumEvent } from "../react/hooks"; +import NoItems from "../react/NoItems"; +import { TypeWidgetProps } from "./type_widget"; const VIEW_TYPES: ViewTypeOptions[] = [ "list", "grid", "presentation" ]; @@ -27,10 +29,12 @@ export default function Book({ note }: TypeWidgetProps) { return ( <> {shouldDisplayNoChildrenWarning && ( - - - + <> + + + + )} - ) + ); } diff --git a/apps/client/src/widgets/type_widgets/Empty.css b/apps/client/src/widgets/type_widgets/Empty.css index 9f04afbcf4..d48ed2b9e1 100644 --- a/apps/client/src/widgets/type_widgets/Empty.css +++ b/apps/client/src/widgets/type_widgets/Empty.css @@ -1,18 +1,20 @@ -.note-detail-empty { - container-type: size; - padding-top: 50px; - min-width: 350px; -} +body.desktop { + .note-detail-empty { + container-type: size; + padding-top: 50px; + min-width: 350px; + } -.note-detail-empty > * { - margin-inline: auto; - max-width: 850px; - padding-inline: 50px; -} - -@container (max-width: 600px) { .note-detail-empty > * { - padding-inline: 20px; + margin-inline: auto; + max-width: 850px; + padding-inline: 50px; + } + + @container (max-width: 600px) { + .note-detail-empty > * { + padding-inline: 20px; + } } } @@ -24,10 +26,22 @@ } .workspace-notes .workspace-note { - width: 130px; text-align: center; margin: 10px; border: 1px transparent solid; + + .workspace-icon { + text-align: center; + font-size: 250%; + } + + @media (min-width: 992px) { + width: 130px; + + .workspace-icon { + font-size: 500%; + } + } } .workspace-notes .workspace-note:hover { @@ -37,8 +51,6 @@ } .note-detail-empty-results .aa-dropdown-menu { - max-height: 50vh; - overflow: scroll; border: var(--bs-border-width) solid var(--bs-border-color); border-top: 0; } @@ -55,8 +67,3 @@ .empty-tab-search .input-clearer-button { border-bottom-right-radius: 0; } - -.workspace-icon { - text-align: center; - font-size: 500%; -} \ No newline at end of file diff --git a/apps/client/src/widgets/type_widgets/ProtectedSession.tsx b/apps/client/src/widgets/type_widgets/ProtectedSession.tsx index 5f7b763501..a735f271e4 100644 --- a/apps/client/src/widgets/type_widgets/ProtectedSession.tsx +++ b/apps/client/src/widgets/type_widgets/ProtectedSession.tsx @@ -34,7 +34,7 @@ export default function ProtectedSession() {