Merge branch 'main' into fix_parens_orderby

This commit is contained in:
Elian Doran
2026-02-21 17:31:33 +02:00
committed by GitHub
266 changed files with 7497 additions and 3734 deletions

View File

@@ -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).

View File

@@ -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 <contact@eliandoran.me>",
"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"
}
}

View File

@@ -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();

View File

@@ -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, {});

View File

@@ -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<Config | null> {
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<void>((res, rej) => {
stream.on("finish", () => res());
stream.on("error", (err) => rej(err));
});
}
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
export async function extractZip(
zipFilePath: string,
outputPath: string,
ignoredFiles?: Set<string>
) {
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`, {

View File

@@ -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> 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();

View File

@@ -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();

View File

@@ -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, "../../../"),

View File

@@ -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 });

View File

@@ -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" }
);
}
}

View File

@@ -1,6 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"include": [],
"include": [
"scripts/**/*.ts"
],
"references": [
{
"path": "../server"

View File

@@ -4,6 +4,7 @@
"entryPoints": [
"src/backend_script_entrypoint.ts"
],
"tsconfig": "tsconfig.app.json",
"plugin": [
"typedoc-plugin-missing-exports"
]

View File

@@ -4,6 +4,7 @@
"entryPoints": [
"src/frontend_script_entrypoint.ts"
],
"tsconfig": "tsconfig.app.json",
"plugin": [
"typedoc-plugin-missing-exports"
]

View File

@@ -27,7 +27,7 @@
"@mermaid-js/layout-elk": "0.2.0",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.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"

View File

@@ -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.

View File

@@ -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(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
.child(<ScrollPadding />)
)
.child(<MobileEditorToolbar />)
.child(new FindWidget())
@@ -102,13 +99,3 @@ export default class MobileLayout {
return rootContainer;
}
}
function FilePropertiesWrapper() {
const { note, ntxId } = useNoteContext();
return (
<div>
{note?.type === "file" && <FilePropertiesTab note={note} ntxId={ntxId} />}
</div>
);
}

View File

@@ -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
};

View File

@@ -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<HTMLElement>) {
export async function executeBundleWithoutErrorHandling(bundle: Bundle, originEntity?: Entity | null, $container?: JQuery<HTMLElement>) {
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<HTMLElement>) {
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);
}
}

View File

@@ -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 = $("<div>");
await renderService.render(entity, $content);
await renderService.render(entity, $content, (e) => {
const $error = $("<div>").addClass("admonition caution").text(typeof e === "string" ? e : getErrorMessage(e));
$content.empty().append($error);
});
$renderedContent.append($content);
} else if (type === "doc" && "noteId" in entity) {

View File

@@ -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<FNoteRow>(`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<FNoteRow>(url, "date-note");
await ws.waitForMaxKnownEntityChangeId();

View File

@@ -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);

View File

@@ -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<HTMLElement>[] = [];
let dismissTimer: ReturnType<typeof setTimeout>;
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<HTMLElement>) {
$el.on("mouseenter", mouseEnterHandler);
$el.on("pointerenter", mouseEnterHandler);
}
async function mouseEnterHandler(this: HTMLElement) {
async function mouseEnterHandler<T>(this: HTMLElement, e: JQuery.TriggeredEvent<T, undefined, T, T>) {
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 = `<div class="note-tooltip-content">${content}</div>`;
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<HTMLElement>, url: string) {
}
let footnoteContent = $targetContent.html();
footnoteContent = `<div class="ck-content">${footnoteContent}</div>`
footnoteContent = `<div class="ck-content">${footnoteContent}</div>`;
return footnoteContent || "";
}

View File

@@ -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<HTMLElement>) {
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<Bundle>(`script/bundle/${renderNoteId}`);
const $scriptContainer = $("<div>");
$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<HTMLElement>) {
// 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
};

View File

@@ -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<HTMLElement>, 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<Bundle>(`script/bundle/${renderNoteId}`);
const $scriptContainer = $("<div>");
$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<HTMLElement>, 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
};

View File

@@ -73,6 +73,10 @@ async function post<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("POST", url, componentId, { data });
}
async function postWithSilentInternalServerError<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("POST", url, componentId, { data, silentInternalServerError: true });
}
async function put<T>(url: string, data?: unknown, componentId?: string) {
return await call<T>("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<T>(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<T>(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<Response> {
function ajax(url: string, method: string, data: unknown, headers: Headers, opts: CallOptions): Promise<Response> {
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,

View File

@@ -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 {

View File

@@ -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%);
}
}

View File

@@ -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%);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -1008,7 +1008,7 @@
"no_attachments": "此笔记没有附件。"
},
"book": {
"no_children_help": "此类型为书籍的笔记没有任何子笔记,因此没有内容显示。请参阅 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 了解详情。",
"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 笔记类型用于<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">编写脚本</a>。简而言之,您有一份 HTML 代码笔记(可包含一些 JavaScript然后这个笔记会把页面渲染出来。要使其正常工作您需要定义一个名为 \"renderNote\" 的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">关系</a>指向要渲染的 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": "了解更多"
}
}

View File

@@ -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 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">Wiki</a> 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. Kurzgesagt, du hast ein HTML-Code-Notiz (optional mit JavaScript) und diese Notiz rendert es. Damit es funktioniert, musst du eine a <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">Beziehung</a> 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"
}
}

View File

@@ -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": "Λοιπά"
}
}

View File

@@ -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 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> 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 isnt 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"
}
}

View File

@@ -669,7 +669,7 @@
"button_exit": "Salir del modo Zen"
},
"sync_status": {
"unknown": "<p>El estado de sincronización será conocido una vez que el siguiente intento de sincronización comience.</p><p>Dé clic para activar la sincronización ahora</p>",
"unknown": "<p>El estado de sincronización será conocido una vez que el siguiente intento de sincronización comience.</p><p>Dé clic para activar la sincronización ahora.</p>",
"connected_with_changes": "<p>Conectado al servidor de sincronización. <br>Hay cambios pendientes que aún no se han sincronizado.</p><p>Dé clic para activar la sincronización.</p>",
"connected_no_changes": "<p>Conectado al servidor de sincronización.<br>Todos los cambios ya han sido sincronizados.</p><p>Dé clic para activar la sincronización.</p>",
"disconnected_with_changes": "<p>El establecimiento de la conexión con el servidor de sincronización no ha tenido éxito.<br>Hay algunos cambios pendientes que aún no se han sincronizado.</p><p>Dé clic para activar la sincronización.</p>",
@@ -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 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relación</a> 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 <a href=\"https://www.electronjs.org/docs/latest/api/accelerator\">documentación de Electron </a> para los modificadores y códigos de tecla disponibles.",
"electron_documentation": "Consulte la <a href=\"https://www.electronjs.org/docs/latest/api/accelerator\">documentación de Electron</a> 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 <code class=\"file-path\"></code> ha sido modificado por última vez en<span class=\"file-last-modified\"></span>.",
"file_last_modified": "El archivo <code class=\"file-path\"></code> ha sido modificado por última vez en <span class=\"file-last-modified\"></span>.",
"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"
}
}

View File

@@ -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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripts</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relation</a> 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": {

View File

@@ -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 <code>now</code> agus <code>parentNote</code> insteallta. Samplaí:\n\n <ul>\n <li><code>Saothair liteartha ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Log le haghaidh ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n\nFéach <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">vicí le sonraí</a>, doiciméid API le haghaidh <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> agus <a href=\"https://day.js.org/docs/en/display/format\">now</a> 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 <code>now</code> agus <code>parentNote</code> insteallta. Samplaí:\n \n <ul>\n <li><code>Saothair liteartha ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Log le haghaidh ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Féach <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">vicí le sonraí</a>, doiciméid API le haghaidh <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> agus <a href=\"https://day.js.org/docs/en/display/format\">now</a> 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 <code>#toc</code> nó <code>#toc=show</code> iallach ar an gClár Ábhair a bheith le feiceáil, cuirfidh <code>#toc=hide</code> 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 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">vicí</a> 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scriptithe</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">gaol</a> 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ó"
}
}

View File

@@ -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}}\""
}
}

View File

@@ -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 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scripting</a>. In breve, si ottiene una nota in codice HTML (opzionalmente con un po' di JavaScript) che verrà visualizzata. Per farla funzionare, è necessario definire una <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relazione</a> 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ù"
}
}

View File

@@ -1860,10 +1860,6 @@
"protecting-title": "保護の状態",
"unprotecting-title": "保護解除の状態"
},
"render": {
"note_detail_render_help_1": "このヘルプートが表示されるのは、このートの「HTML のレンダリング」タイプには、正常に機能するために必要なリレーションがないためです。",
"note_detail_render_help_2": "レンダリングHTMLートタイプは、<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">スクリプティング</a>に使用されます。簡単に言うと、HTMLコードートオプションでJavaScriptを含むがあり、このートがそれをレンダリングします。これを動作させるには、レンダリングするHTMLートを指す「renderNote」という<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">リレーション</a>を定義する必要があります。"
},
"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": "このコレクションには子ノートがないため、表示するものがありません。詳細は<a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a>をご覧ください。",
"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": "さらに詳しく"
}
}

View File

@@ -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": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"노트로 이동\" 대화 상자</a> 표시",
"scrollToActiveNote": "활성화된 노트로 스크롤 이동"
}
}

View File

@@ -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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">skryptowania</a>. W skrócie, masz notatkę kodu HTML (opcjonalnie z JavaScript) i ta notatka ją wyrenderuje. Aby to zadziałało, musisz zdefiniować <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relację</a> o nazwie \"renderNote\" wskazującą na notatkę HTML do wyrenderowania."
},
"backend_log": {
"refresh": "Odśwież"
},

View File

@@ -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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> 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": {

View File

@@ -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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">automação</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relação</a> 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": {

View File

@@ -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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">scriptare</a>. 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 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">relație</a> 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": {

View File

@@ -2081,10 +2081,6 @@
"help-button": {
"title": "Открыть соответствующую страницу справки"
},
"render": {
"note_detail_render_help_2": "Тип заметки «Рендер HTML» используется для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптинга</a>. Если коротко, у вас есть заметка с HTML-кодом (возможно, с добавлением JavaScript), и эта заметка её отобразит. Для этого необходимо определить <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">отношение</a> с именем «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}} атрибут",

View File

@@ -1007,7 +1007,7 @@
"no_attachments": "此筆記沒有附件。"
},
"book": {
"no_children_help": "此類型為書籍的筆記沒有任何子筆記,因此沒有內容可顯示。請參閱 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 以了解詳情。",
"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": "渲染筆記類型用於編寫 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">腳本</a>。簡單說就是您可以寫HTML程式碼或者加上一些JavaScript程式碼 然後這個筆記會把頁面渲染出來。要使其正常工作,您需要定義一個名為 \"renderNote\" 的 <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">關聯</a> 指向要呈現的 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": "了解更多"
}
}

View File

@@ -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 використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/scripts.html\">скриптів</a>. Коротше кажучи, у вас є нотатка з HTML-кодом (за бажанням з деяким JavaScript), і ця нотатка її відобразить. Щоб це запрацювало, вам потрібно визначити <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/attributes.html\">відношення</a> під назвою \"renderNote\", яке вказує на нотатку HTML для відображення."
},
"backend_log": {
"refresh": "Оновити"
},
@@ -2067,7 +2063,6 @@
"app-restart-required": "(щоб зміни набули чинності, потрібен перезапуск програми)"
},
"pagination": {
"page_title": "Сторінка {{startIndex}} - {{endIndex}}",
"total_notes": "{{count}} нотаток"
},
"collections": {

View File

@@ -63,11 +63,13 @@ declare global {
declare module "preact" {
namespace JSX {
interface ElectronWebViewElement extends JSX.HTMLAttributes<HTMLElement> {
src: string;
class: string;
}
interface IntrinsicElements {
webview: {
src: string;
class: string;
}
webview: ElectronWebViewElement;
}
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
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<LabelType, HTMLInputTypeAttribute> = {
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<string[]>(`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<string[]>(`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<StateUpdater<Cell[] | undefined>>) {
if (value === cell.valueAttr.value) return;
const { attributeId } = await server.put<UpdateAttributeResponse>(
`notes/${note.noteId}/attribute`,
{

View File

@@ -47,6 +47,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
>
{isMobile() && <>
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
<MenuItem command="showRecentChanges" icon="bx bx-history" text={t("recent_changes.title")} />
<FormDropdownDivider />
</>}

View File

@@ -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 */

View File

@@ -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;
}
}
}

View File

@@ -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<PaginationContext, "pageNotes">) {
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((
<a
href="javascript:"
title={t("pagination.page_title", { startIndex, endIndex })}
onClick={() => setPage(i)}
>
{i}
</a>
))
} else {
// Current page
children.push(<span className="current-page">{i}</span>)
}
children.push(<>{" "}&nbsp;{" "}</>);
} else if (lastPrinted) {
children.push(<>{"... "}&nbsp;{" "}</>);
lastPrinted = false;
}
}
return (
<div class="note-list-pager">
{children}
<div className="note-list-pager-container">
<div className="note-list-pager">
<ActionButton
icon="bx bx-chevron-left"
className="note-list-pager-nav-button"
disabled={(page === 1)}
text={t("pagination.prev_page")}
onClick={() => setPage(page - 1)}
/>
<span className="note-list-pager-total-count">({t("pagination.total_notes", { count: totalNotes })})</span>
<PageButtons page={page} setPage={setPage} pageCount={pageCount} />
<div className="note-list-pager-narrow-counter">
<strong>{page}</strong> / <strong>{pageCount}</strong>
</div>
<ActionButton
icon="bx bx-chevron-right"
className="note-list-pager-nav-button"
disabled={(page === pageCount)}
text={t("pagination.next_page")}
onClick={() => setPage(page + 1)}
/>
<div className="note-list-pager-total-count">
{t("pagination.total_notes", { count: totalNotes })}
</div>
</div>
</div>
)
}
interface PageButtonsProps {
page: number;
setPage: Dispatch<StateUpdater<number>>;
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 <div className={clsx("note-list-pager-page-button-container", {
"note-list-pager-ellipsis-present": (totalButtonCount === maxButtonCount)
})}
style={{"--note-list-pager-page-button-count": totalButtonCount}}>
{[
...createSegment(leftStart, leftLength, props.page, props.setPage, false),
...createSegment(middleStart, middleLength, props.page, props.setPage, hasLeadingEllipsis),
...createSegment(rightStart, rightLength, props.page, props.setPage, hasTrailingEllipsis),
]}
</div>;
}
function createSegment(start: number, length: number, currentPage: number, setPage: Dispatch<StateUpdater<number>>, prependEllipsis: boolean): ComponentChildren[] {
const children: ComponentChildren[] = [];
if (prependEllipsis) {
children.push(<span className="note-list-pager-ellipsis">...</span>);
}
for (let i = 0; i < length; i++) {
const pageNum = start + i;
const isCurrent = (pageNum === currentPage);
children.push((
<Button
text={pageNum.toString()}
kind="lowProfile"
className={clsx(
"note-list-pager-page-button",
{"note-list-pager-page-button-current": isCurrent}
)}
disabled={isCurrent}
onClick={() => setPage(pageNum)}
/>
));
}
return children;
}
export function usePagination(note: FNote, noteIds: string[]): PaginationContext {
const [ page, setPage ] = useState(1);
const [ pageNotes, setPageNotes ] = useState<FNote[]>();

View File

@@ -307,11 +307,11 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean, c
// Called upon when clicking the day number in the calendar, opens or creates the day note but only if in a calendar root.
const onDateClick = useCallback(async (e: DateClickArg) => {
const eventNote = await date_notes.getDayNote(e.dateStr);
const eventNote = await date_notes.getDayNote(e.dateStr, note.noteId);
if (eventNote) {
appContext.triggerCommand("openInPopup", { noteIdOrPath: eventNote.noteId });
}
}, []);
}, [ note ]);
return {
select: onCalendarSelection,

View File

@@ -7,13 +7,16 @@
> .collection-properties {
position: relative;
z-index: 998;
}
}
.geo-map-container {
height: 100%;
overflow: hidden;
.maplibregl-canvas-container {
position: relative;
}
}
.leaflet-pane {

View File

@@ -22,7 +22,7 @@ import { ViewModeProps } from "../interface";
import { createNewNote, moveMarker } from "./api";
import openContextMenu, { openMapContextMenu } from "./context_menu";
import Map from "./map";
import { DEFAULT_MAP_LAYER_NAME } from "./map_layer";
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, MapLayer } from "./map_layer";
import Marker, { GpxTrack } from "./marker";
const DEFAULT_COORDINATES: [number, number] = [3.878638227135724, 446.6630455551659];
@@ -45,10 +45,11 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
const [ state, setState ] = useState(State.Normal);
const [ coordinates, setCoordinates ] = useState(viewConfig?.view?.center);
const [ zoom, setZoom ] = useState(viewConfig?.view?.zoom);
const [ layerName ] = useNoteLabel(note, "map:style");
const [ hasScale ] = useNoteLabelBoolean(note, "map:scale");
const [ hideLabels ] = useNoteLabelBoolean(note, "map:hideLabels");
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const [ notes, setNotes ] = useState<FNote[]>([]);
const layerData = useLayerData(note);
const spacedUpdate = useSpacedUpdate(() => {
if (viewConfig) {
saveConfig(viewConfig);
@@ -152,7 +153,7 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
apiRef={apiRef} containerRef={containerRef}
coordinates={coordinates}
zoom={zoom}
layerName={layerName ?? DEFAULT_MAP_LAYER_NAME}
layerData={layerData}
viewportChanged={(coordinates, zoom) => {
if (!viewConfig) viewConfig = {};
viewConfig.view = { center: coordinates, zoom };
@@ -162,13 +163,35 @@ export default function GeoView({ note, noteIds, viewConfig, saveConfig }: ViewM
onContextMenu={onContextMenu}
scale={hasScale}
>
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} />)}
{notes.map(note => <NoteWrapper note={note} isReadOnly={isReadOnly} hideLabels={hideLabels} />)}
</Map>}
<GeoMapTouchBar state={state} map={apiRef.current} />
</div>
);
}
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 <NoteGpxTrack note={note} />;
return <NoteGpxTrack note={note} hideLabels={hideLabels} />;
}
if (location) {
const latLng = location?.split(",", 2).map((el) => parseFloat(el)) as [ number, number ] | undefined;
if (!latLng) return;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} />;
return <NoteMarker note={note} editable={!isReadOnly} latLng={latLng} hideLabels={hideLabels} />;
}
}
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<string>();
const blob = useNoteBlob(note);
@@ -256,7 +284,7 @@ function NoteGpxTrack({ note }: { note: FNote }) {
const options = useMemo<GPXOptions>(() => ({
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 && <GpxTrack gpxXmlString={xmlString} options={options} />;
}

View File

@@ -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<HTMLDivElement>;
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<L.Map>(null);
const containerRef = useSyncedRef<HTMLDivElement>(_containerRef);
@@ -49,8 +49,6 @@ export default function Map({ coordinates, zoom, layerName, viewportChanged, chi
const [ layer, setLayer ] = useState<Layer>();
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 (
<div
ref={containerRef}
className={`geo-map-container ${MAP_LAYERS[layerName].isDarkTheme ? "dark" : ""}`}
className={`geo-map-container ${layerData.isDarkTheme ? "dark" : ""}`}
>
<ParentMap.Provider value={mapRef.current}>
{children}

View File

@@ -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<string, VectorLayer | RasterLayer> = {
export const MAP_LAYERS: Record<string, MapLayer> = {
"openstreetmap": {
name: "OpenStreetMap",
type: "raster",

View File

@@ -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;
}

View File

@@ -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 && <div class="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<div class="note-list-container use-tn-links">
<Card className={clsx("nested-note-list", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<ListNoteCard
key={childNote.noteId}
@@ -41,7 +47,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
expandDepth={expandDepth} highlightedTokens={highlightedTokens}
currentLevel={1} includeArchived={includeArchived} />
))}
</div>
</Card>
<Pager {...pagination} />
</div>}
@@ -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 = <>
<CardSection className="note-content-preview">
<NoteContent note={note}
highlightedTokens={highlightedTokens}
noChildrenList
includeArchivedNotes={includeArchived} />
</CardSection>
<NoteChildren note={note}
parentNote={parentNote}
highlightedTokens={highlightedTokens}
currentLevel={currentLevel}
expandDepth={expandDepth}
includeArchived={includeArchived} />
</>
}
return (
<div
className={`note-book-card no-tooltip-preview ${isExpanded ? "expanded" : ""} ${note.isArchived ? "archived" : ""}`}
<CardSection
className={clsx("nested-note-list-item", "no-tooltip-preview", note.getColorClass(), {
"expanded": isExpanded,
"archived": note.isArchived
})}
subSections={subSections}
subSectionsVisible={isExpanded}
highlightOnHover
data-note-id={note.noteId}
>
<h5 className="note-book-header">
<span
className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
onClick={() => setExpanded(!isExpanded)}
/>
<h5>
<span className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
onClick={() => setExpanded(!isExpanded)}/>
<Icon className="note-icon" icon={note.getIcon()} />
<NoteLink className="note-book-title" notePath={notePath} noPreview showNotePath={parentNote.type === "search"} highlightedTokens={highlightedTokens} />
<NoteLink className="note-book-title"
notePath={notePath}
noPreview
showNotePath={parentNote.type === "search"}
highlightedTokens={highlightedTokens} />
<NoteAttributes note={note} />
<ActionButton className="nested-note-list-item-menu"
icon="bx bx-dots-vertical-rounded" text=""
onClick={(e) => openNoteMenu(notePath, e)}
/>
</h5>
{isExpanded && <>
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
</>}
</div>
</CardSection>
);
}
@@ -165,6 +196,9 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
const [ready, setReady] = useState(false);
const [noteType, setNoteType] = useState<string>("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 <div ref={contentRef} className="note-book-content" />;
return <div ref={contentRef} className={clsx("note-book-content", `type-${noteType}`, {"note-book-content-ready": ready})} />;
}
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<HTMLElement>) {
linkContextMenuService.openContextMenu(notePath, e);
e.stopPropagation()
}

View File

@@ -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);

View File

@@ -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 {

View File

@@ -67,10 +67,7 @@ export default function PopupEditor() {
<NoteContextContext.Provider value={noteContext}>
<DialogWrapper>
<Modal
title={<>
<TitleRow />
{isNewLayout && <NoteBadges />}
</>}
title={<TitleRow />}
customTitleBarButtons={[{
iconClassName: "bx-expand-alt",
title: t("popup-editor.maximize"),
@@ -123,6 +120,7 @@ export function TitleRow() {
<div className="title-row">
<NoteIcon />
<NoteTitleWidget />
{isNewLayout && <NoteBadges />}
</div>
);
}

View File

@@ -57,7 +57,7 @@ export default function BulkActionsDialog() {
className="bulk-actions-dialog"
size="xl"
title={t("bulk_actions.bulk_actions")}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} primary />}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} kind="primary" />}
show={shown}
onSubmit={async () => {
await server.post("bulk-action/execute", {

View File

@@ -72,7 +72,7 @@ export default function DeleteNotesDialog() {
footer={<>
<Button text={t("delete_notes.cancel")}
onClick={() => setShown(false)} />
<Button text={t("delete_notes.ok")} primary
<Button text={t("delete_notes.ok")} kind="primary"
buttonRef={okButtonRef}
onClick={() => {
opts.callback?.({ proceed: true, deleteAllClones, eraseNotes });

View File

@@ -58,7 +58,7 @@ export default function ExportDialog() {
setShown(false);
}}
onHidden={() => setShown(false)}
footer={<Button className="export-button" text={t("export.export")} primary />}
footer={<Button className="export-button" text={t("export.export")} kind="primary" />}
show={shown}
>

View File

@@ -55,7 +55,7 @@ export default function ImportDialog() {
setShown(false);
setFiles(null);
}}
footer={<Button text={t("import.import")} primary disabled={!files} />}
footer={<Button text={t("import.import")} kind="primary" disabled={!files} />}
show={shown}
>
<FormGroup name="files" label={t("import.chooseImportFile")} description={

View File

@@ -69,7 +69,7 @@ export default function PromptDialog() {
submitValue.current = null;
opts.current = undefined;
}}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" kind="primary" />}
show={shown}
stackable
>

View File

@@ -203,7 +203,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
}} />
&nbsp;
<Button
primary
kind="primary"
icon="bx bx-download"
text={t("revisions.download_button")}
onClick={() => {

View File

@@ -35,7 +35,7 @@ export default function UploadAttachmentsDialog() {
className="upload-attachments-dialog"
size="lg"
title={t("upload_attachments.upload_attachments_to_note")}
footer={<Button text={t("upload_attachments.upload")} primary disabled={!files || isUploading} />}
footer={<Button text={t("upload_attachments.upload")} kind="primary" disabled={!files || isUploading} />}
onSubmit={async () => {
if (!files || !parentNoteId) {
return;

View File

@@ -1,12 +1,14 @@
import { useEffect, useRef, useState } from "preact/hooks";
import "./SyncStatus.css";
import { t } from "../../services/i18n";
import clsx from "clsx";
import { escapeQuotes } from "../../services/utils";
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
import sync from "../../services/sync";
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
import { WebSocketMessage } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n";
import sync from "../../services/sync";
import { escapeQuotes } from "../../services/utils";
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
type SyncState = "unknown" | "in-progress"
| "connected-with-changes" | "connected-no-changes"
@@ -53,29 +55,29 @@ export default function SyncStatus() {
const spanRef = useRef<HTMLSpanElement>(null);
const [ syncServerHost ] = useTriliumOption("syncServerHost");
useStaticTooltip(spanRef, {
html: true
// TODO: Placement
html: true,
title: escapeQuotes(title)
});
return (syncServerHost &&
<div class="sync-status-widget launcher-button">
<div class="sync-status">
<span
key={syncState} // Force re-render when state changes to update tooltip content.
ref={spanRef}
className={clsx("sync-status-icon", `sync-status-${syncState}`, icon)}
title={escapeQuotes(title)}
onClick={() => {
if (syncState === "in-progress") return;
sync.syncNow();
}}
>
{hasChanges && (
<span class="bx bxs-star sync-status-sub-icon"></span>
<span class="bx bxs-star sync-status-sub-icon" />
)}
</span>
</div>
</div>
)
);
}
function useSyncStatus() {

View File

@@ -3,11 +3,14 @@ import { createContext } from "preact";
import { useContext } from "preact/hooks";
import FNote from "../../entities/fnote";
import utils from "../../services/utils";
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import { useNoteLabel, useNoteProperty } from "../react/hooks";
import Icon from "../react/Icon";
const cachedIsMobile = utils.isMobile();
export const LaunchBarContext = createContext<{
isHorizontalLayout: boolean;
}>({
@@ -26,7 +29,7 @@ export function LaunchBarActionButton({ className, ...props }: Omit<ActionButton
<ActionButton
className={clsx("button-widget launcher-button", className)}
noIconActionClass
titlePosition={isHorizontalLayout ? "bottom" : "right"}
titlePosition={getTitlePosition(isHorizontalLayout)}
{...props}
/>
);
@@ -34,6 +37,7 @@ export function LaunchBarActionButton({ className, ...props }: Omit<ActionButton
export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions" | "dropdownRef"> & { icon: string }) {
const { isHorizontalLayout } = useContext(LaunchBarContext);
const titlePosition = getTitlePosition(isHorizontalLayout);
return (
<Dropdown
@@ -41,12 +45,12 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
buttonClassName="right-dropdown-button launcher-button"
hideToggleArrow
text={<Icon icon={icon} />}
titlePosition={isHorizontalLayout ? "bottom" : "right"}
titlePosition={titlePosition}
titleOptions={{ animation: false }}
dropdownOptions={{
...dropdownOptions,
popperConfig: {
placement: isHorizontalLayout ? "bottom" : "right"
placement: titlePosition
}
}}
mobileBackdrop
@@ -67,3 +71,10 @@ export function useLauncherIconAndTitle(note: FNote) {
title: title ?? ""
};
}
function getTitlePosition(isHorizontalLayout: boolean) {
if (cachedIsMobile) {
return "top";
}
return isHorizontalLayout ? "bottom" : "right";
}

View File

@@ -0,0 +1,284 @@
import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { openInAppHelpFromUrl } from "../../services/utils";
import { BadgeWithDropdown } from "../react/Badge";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import FormToggle from "../react/FormToggle";
import { useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks";
import { BookProperty, ViewProperty } from "../react/NotePropertyMenu";
const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ];
const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name));
const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const;
interface ActiveContentInfo {
type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme";
isEnabled: boolean;
canToggleEnabled: boolean;
}
const executeOption: BookProperty = {
type: "button",
icon: "bx bx-play",
label: t("active_content_badges.menu_execute_now"),
onClick: context => context.triggerCommand("runActiveNote")
};
const typeMappings: Record<ActiveContentInfo["type"], {
title: string;
icon: string;
helpPage: string;
apiDocsPage?: string;
isExecutable?: boolean;
additionalOptions?: BookProperty[];
}> = {
iconPack: {
title: t("active_content_badges.type_icon_pack"),
icon: "bx bx-package",
helpPage: "g1mlRoU8CsqC",
},
backendScript: {
title: t("active_content_badges.type_backend_script"),
icon: "bx bx-server",
helpPage: "SPirpZypehBG",
apiDocsPage: "MEtfsqa5VwNi",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") },
{ value: "daily", label: t("active_content_badges.menu_run_daily") },
{ value: "hourly", label: t("active_content_badges.menu_run_hourly") }
]
}
]
},
frontendScript: {
title: t("active_content_badges.type_frontend_script"),
icon: "bx bx-window",
helpPage: "yIhgI5H7A2Sm",
apiDocsPage: "Q2z6av6JZVWm",
isExecutable: true,
additionalOptions: [
executeOption,
{
type: "combobox",
bindToLabel: "run",
label: t("active_content_badges.menu_run"),
icon: "bx bx-rss",
dropStart: true,
options: [
{ value: null, label: t("active_content_badges.menu_run_disabled") },
{ value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") },
{ value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") },
]
},
{ type: "separator" },
{
type: "button",
label: t("active_content_badges.menu_change_to_widget"),
icon: "bx bxs-widget",
onClick: ({ note }) => attributes.setLabel(note.noteId, "widget")
}
]
},
widget: {
title: t("active_content_badges.type_widget"),
icon: "bx bxs-widget",
helpPage: "MgibgPcfeuGz",
additionalOptions: [
{
type: "button",
label: t("active_content_badges.menu_change_to_frontend_script"),
icon: "bx bx-window",
onClick: ({ note }) => {
attributes.removeOwnedLabelByName(note, "widget");
attributes.removeOwnedLabelByName(note, "disabled:widget");
}
}
]
},
appCss: {
title: t("active_content_badges.type_app_css"),
icon: "bx bxs-file-css",
helpPage: "AlhDUqhENtH7"
},
renderNote: {
title: t("active_content_badges.type_render_note"),
icon: "bx bx-extension",
helpPage: "HcABDtFCkbFN"
},
webView: {
title: t("active_content_badges.type_web_view"),
icon: "bx bx-globe",
helpPage: "1vHRoWCEjj0L"
},
appTheme: {
title :t("active_content_badges.type_app_theme"),
icon: "bx bx-palette",
helpPage: "7NfNr5pZpVKV",
additionalOptions: [
{
type: "combobox",
bindToLabel: "appThemeBase",
label: t("active_content_badges.menu_theme_base"),
icon: "bx bx-layer",
dropStart: true,
options: [
{ label: t("theme.auto_theme"), value: null },
{ type: "separator" },
{ label: t("theme.triliumnext"), value: "next" },
{ label: t("theme.triliumnext-light"), value: "next-light" },
{ label: t("theme.triliumnext-dark"), value: "next-dark" }
]
}
]
}
};
export function ActiveContentBadges() {
const { note } = useNoteContext();
const info = useActiveContentInfo(note);
return (note && info &&
<>
{info.canToggleEnabled && <ActiveContentToggle info={info} note={note} />}
<ActiveContentBadge info={info} note={note} />
</>
);
}
function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) {
const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type];
return (
<BadgeWithDropdown
className={clsx("active-content-badge", info.canToggleEnabled && !info.isEnabled && "disabled")}
icon={icon}
text={title}
dropdownOptions={{
dropdownContainerClassName: "mobile-bottom-menu",
mobileBackdrop: true
}}
>
{additionalOptions?.length && (
<>
{additionalOptions?.map((property, i) => (
<ViewProperty key={i} note={note} property={property} />
))}
<FormDropdownDivider />
</>
)}
<FormListItem
icon="bx bx-help-circle"
onClick={() => openInAppHelpFromUrl(helpPage)}
>{t("active_content_badges.menu_docs")}</FormListItem>
{apiDocsPage && <FormListItem
icon="bx bx-book-content"
onClick={() => openInAppHelpFromUrl(apiDocsPage)}
>{t("code_buttons.trilium_api_docs_button_title")}</FormListItem>}
</BadgeWithDropdown>
);
}
function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) {
const { title } = typeMappings[info.type];
return info && <FormToggle
switchOnName="" switchOffName=""
currentValue={info.isEnabled}
switchOffTooltip={t("active_content_badges.toggle_tooltip_disable_tooltip", { type: title })}
switchOnTooltip={t("active_content_badges.toggle_tooltip_enable_tooltip", { type: title })}
onChange={async (willEnable) => {
await Promise.all(note.getOwnedAttributes()
.map(attr => ({ name: attributes.getNameWithoutDangerousPrefix(attr.name), type: attr.type }))
.filter(({ name, type }) => DANGEROUS_ATTRIBUTES.some(item => item.name === name && item.type === type))
.map(({ name, type }) => attributes.toggleDangerousAttribute(note, type, name, willEnable)));
}}
/>;
}
function useActiveContentInfo(note: FNote | null | undefined) {
const [ info, setInfo ] = useState<ActiveContentInfo | null>(null);
const noteType = useNoteProperty(note, "type");
const noteMime = useNoteProperty(note, "mime");
function refresh() {
let type: ActiveContentInfo["type"] | null = null;
let isEnabled = false;
let canToggleEnabled = false;
if (!note) {
setInfo(null);
return;
}
if (noteType === "render") {
type = "renderNote";
isEnabled = note.hasRelation("renderNote");
} else if (noteType === "webView") {
type = "webView";
isEnabled = note.hasLabel("webViewSrc");
} else if (noteType === "code" && noteMime === "application/javascript;env=backend") {
type = "backendScript";
for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) {
isEnabled ||= note.hasLabel(backendLabel);
if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) {
canToggleEnabled = true;
}
}
} else if (noteType === "code" && noteMime === "application/javascript;env=frontend") {
type = "frontendScript";
isEnabled = note.hasLabel("widget") || note.hasLabel("run");
canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run");
} else if (noteType === "code" && note.hasLabelOrDisabled("appTheme")) {
isEnabled = note.hasLabel("appTheme");
canToggleEnabled = true;
}
for (const labelToCheck of activeContentLabels) {
if (note.hasLabel(labelToCheck)) {
type = labelToCheck;
isEnabled = true;
canToggleEnabled = true;
break;
} else if (note.hasLabel(`disabled:${labelToCheck}`)) {
type = labelToCheck;
isEnabled = false;
canToggleEnabled = true;
break;
}
}
if (type) {
setInfo({ type, isEnabled, canToggleEnabled });
} else {
setInfo(null);
}
}
// Refresh on note change.
useEffect(refresh, [ note, noteType, noteMime ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
refresh();
}
});
return info;
}

View File

@@ -19,6 +19,9 @@
--link-hover-background: var(--icon-button-hover-background);
color: var(--custom-color, inherit);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
color: var(--custom-color, inherit);

View File

@@ -37,6 +37,10 @@
pointer-events: none;
}
}
&.active-content-badge { --color: var(--badge-active-content-background-color); }
&.active-content-badge.disabled {
opacity: 0.5;
}
min-width: 0;
@@ -45,6 +49,11 @@
text-overflow: ellipsis;
min-width: 0;
}
.switch-button {
--switch-track-height: 8px;
--switch-track-width: 30px;
}
}
.dropdown-badge {

View File

@@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks";
import { useShareState } from "../ribbon/BasicPropertiesTab";
import { useShareInfo } from "../shared_info";
import { ActiveContentBadges } from "./ActiveContentBadges";
export default function NoteBadges() {
return (
@@ -19,6 +20,7 @@ export default function NoteBadges() {
<ShareBadge />
<ClippedNoteBadge />
<ExecuteBadge />
<ActiveContentBadges />
</div>
);
}

View File

@@ -414,7 +414,7 @@ function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
const dropdownRef = useRef<BootstrapDropdown>(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());

View File

@@ -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);

View File

@@ -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<BasicWidget> {
private screenName: Screen;
@@ -54,7 +54,7 @@ export default class SidebarContainer extends FlexContainer<BasicWidget> {
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<BasicWidget> {
}
} 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<BasicWidget> {
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;

View File

@@ -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() && <ActionButton
icon="bx bx-sidebar"
text={t("note_tree.toggle-sidebar")}
onClick={() => 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"
});
}}
/>}
</div>
)
);
}

View File

@@ -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 {

View File

@@ -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<ViewTypeOptions, string> = {
@@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
<Dropdown
buttonClassName="bx bx-cog icon-action"
hideToggleArrow
dropdownContainerClassName="mobile-bottom-menu"
mobileBackdrop
>
{properties.map(property => (
<ViewProperty key={property.label} note={note} property={property} />
{properties.map((property, index) => (
<ViewProperty key={index} note={note} property={property} />
))}
{properties.length > 0 && <FormDropdownDivider />}
@@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
</Dropdown>
);
}
function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
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 &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
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 (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? "");
function renderItem(option: ComboBoxItem) {
return (
<FormListItem
key={option.value}
checked={value === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={value}
onChange={setValue}
/>
);
}

View File

@@ -69,7 +69,7 @@ function MobileNoteIconSwitcher({ note, icon }: {
const [ modalShown, setModalShown ] = useState(false);
const { windowWidth } = useWindowSize();
return (note &&
return (
<div className="note-icon-widget">
<ActionButton
className="note-icon"
@@ -86,7 +86,7 @@ function MobileNoteIconSwitcher({ note, icon }: {
className="icon-switcher note-icon-widget"
scrollable
>
<NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />
{note && <NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />}
</Modal>
), document.body)}
</div>

View File

@@ -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<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu" | "style"> {
@@ -17,6 +18,8 @@ export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement
disabled?: boolean;
}
const cachedIsMobile = isMobile();
export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null);
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
@@ -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
});

View File

@@ -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) => (
<>

View File

@@ -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;
}
}
}

View File

@@ -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 <div className={clsx("tn-card", props.className)}>
{props.heading && <h5 class="tn-card-heading">{props.heading}</h5>}
<div className="tn-card-body">
{props.children}
</div>
</div>;
}
// #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<CardSectionContextType | undefined>(undefined);
export function CardSection(props: {children: ComponentChildren} & CardSectionProps) {
const parentContext = useContext(CardSectionContext);
const nestingLevel = (parentContext && parentContext.nestingLevel + 1) ?? 0;
return <>
<section className={clsx("tn-card-section", props.className, {
"tn-card-section-nested": nestingLevel > 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}
</section>
{props.subSectionsVisible && props.subSections &&
<CardSectionContext.Provider value={{nestingLevel}}>
{props.subSections}
</CardSectionContext.Provider>
}
</>;
}
// #endregion

View File

@@ -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<boolean>;
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<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
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<string>;
/**
* 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 <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckBoxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
case "separator":
return <FormDropdownDivider />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
return (
<FormListItem
icon={property.icon}
title={property.title}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
>{property.label}</FormListItem>
);
}
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 &&
<FormDropdownSubmenu
icon={property.icon ?? "bx bx-empty"}
title={property.label}
onDropdownToggleClicked={() => clickContext && property.onClick(clickContext)}
>
<ItemsComponent note={note} parentComponent={parentComponent} />
</FormDropdownSubmenu>
);
}
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 (
<FormListItem
icon={property.icon}
disabled={disabled}
onClick={(e) => e.stopPropagation()}
>
{property.label}
<FormTextBox
type="number"
currentValue={value ?? ""} onChange={setValue}
style={{ width: (property.width ?? 100) }}
min={property.min ?? 0}
disabled={disabled}
/>
</FormListItem>
);
}
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 (
<FormListItem
key={option.value}
checked={valueWithDefault === option.value}
onClick={() => setValue(option.value)}
>
{option.label}
</FormListItem>
);
}
return (
<FormDropdownSubmenu
title={property.label}
icon={property.icon ?? "bx bx-empty"}
dropStart={property.dropStart}
>
{(property.options).map((option, index) => {
if ("items" in option) {
return (
<Fragment key={option.title}>
<FormListItem key={option.title} disabled>{option.title}</FormListItem>
{option.items.map(renderItem)}
{index < property.options.length - 1 && <FormDropdownDivider />}
</Fragment>
);
}
if ("type" in option) {
return <FormDropdownDivider key={index} />;
}
return renderItem(option);
})}
</FormDropdownSubmenu>
);
}
function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormListToggleableItem
icon={property.icon}
title={property.label}
currentValue={ property.reverseValue ? !value : value }
onChange={newValue => setValue(property.reverseValue ? !newValue : newValue)}
/>
);
}

View File

@@ -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;
}

View File

@@ -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<ViewTypeOptions, string> = {
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 (
<div style={{ display: "flex", alignItems: "baseline" }}>
<span style={{ whiteSpace: "nowrap" }}>{t("book_properties.view_type")}:&nbsp; &nbsp;</span>
<FormSelect
currentValue={viewType ?? "grid"} onChange={setViewType}
values={collectionTypes}
keyProperty="key" titleProperty="value"
/>
</div>
)
return (
<div style={{ display: "flex", alignItems: "baseline" }}>
<span style={{ whiteSpace: "nowrap" }}>{t("book_properties.view_type")}:&nbsp; &nbsp;</span>
<FormSelect
currentValue={viewType ?? "grid"} onChange={setViewType}
values={collectionTypes}
keyProperty="key" titleProperty="value"
/>
</div>
);
}
function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
return (
<>
{properties.map(property => (
<div className={`type-${property}`}>
{mapPropertyView({ note, property })}
</div>
))}
function BookProperties({ note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) {
return (
<>
{properties.map((property, index) => (
<div key={index} className={`type-${property}`}>
{mapPropertyView({ note, property })}
</div>
))}
<CheckboxPropertyView
note={note} property={{
bindToLabel: "includeArchived",
label: t("book_properties.include_archived_notes"),
type: "checkbox"
}}
/>
</>
)
<CheckboxPropertyView
note={note} property={{
bindToLabel: "includeArchived",
label: t("book_properties.include_archived_notes"),
type: "checkbox"
}}
/>
</>
);
}
function mapPropertyView({ note, property }: { note: FNote, property: BookProperty }) {
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />
case "checkbox":
return <CheckboxPropertyView note={note} property={property} />
case "number":
return <NumberPropertyView note={note} property={property} />
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />
}
switch (property.type) {
case "button":
return <ButtonPropertyView note={note} property={property} />;
case "split-button":
return <SplitButtonPropertyView note={note} property={property} />;
case "checkbox":
return <CheckboxPropertyView note={note} property={property} />;
case "number":
return <NumberPropertyView note={note} property={property} />;
case "combobox":
return <ComboBoxPropertyView note={note} property={property} />;
}
}
function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) {
const parentComponent = useContext(ParentComponent);
const parentComponent = useContext(ParentComponent);
return <Button
text={property.label}
title={property.title}
icon={property.icon}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
/>
return <Button
text={property.label}
title={property.title}
icon={property.icon}
onClick={() => {
if (!parentComponent) return;
property.onClick({
note,
triggerCommand: parentComponent.triggerCommand.bind(parentComponent)
});
}}
/>;
}
function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) {
@@ -131,18 +133,18 @@ function SplitButtonPropertyView({ note, property }: { note: FNote, property: Sp
onClick={() => clickContext && property.onClick(clickContext)}
>
{parentComponent && <ItemsComponent note={note} parentComponent={parentComponent} />}
</SplitButton>
</SplitButton>;
}
function CheckboxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) {
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel);
return (
<FormCheckbox
label={property.label}
currentValue={value} onChange={setValue}
/>
)
return (
<FormCheckbox
label={property.label}
currentValue={value} onChange={setValue}
/>
);
}
function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) {
@@ -160,7 +162,7 @@ function NumberPropertyView({ note, property }: { note: FNote, property: NumberP
disabled={disabled}
/>
</LabelledEntry>
)
);
}
function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) {
@@ -169,12 +171,12 @@ function ComboBoxPropertyView({ note, property }: { note: FNote, property: Combo
return (
<LabelledEntry label={property.label}>
<FormSelectWithGroups
values={property.options}
values={property.options.filter(i => !("type" in i)) as (ComboBoxItem | ComboBoxGroup)[]}
keyProperty="value" titleProperty="label"
currentValue={value ?? property.defaultValue} onChange={setValue}
/>
</LabelledEntry>
)
);
}
function LabelledEntry({ label, children }: { label: string, children: ComponentChildren }) {
@@ -186,5 +188,5 @@ function LabelledEntry({ label, children }: { label: string, children: Component
{children}
</label>
</>
)
);
}

View File

@@ -43,7 +43,7 @@ export default function FilePropertiesTab({ note, ntxId }: Pick<TabContext, "not
<Button
icon="bx bx-download"
text={t("file_properties.download")}
primary
kind="primary"
disabled={!canAccessProtectedNote}
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>

View File

@@ -43,7 +43,7 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
<Button
text={t("image_properties.download")}
icon="bx bx-download"
primary
kind="primary"
onClick={() => downloadFileNote(note, parentComponent, ntxId)}
/>

View File

@@ -1,17 +1,18 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import FAttribute from "../../entities/fattribute";
import { useLegacyWidget, useTriliumEvent } from "../react/hooks";
import attribute_renderer from "../../services/attribute_renderer";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import attribute_renderer from "../../services/attribute_renderer";
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
import { useLegacyWidget, useTriliumEvent } from "../react/hooks";
import RawHtml from "../react/RawHtml";
import { joinElements } from "../react/react_utils";
import AttributeDetailWidget from "../attribute_widgets/attribute_detail";
import { TabContext } from "./ribbon-interface";
type InheritedAttributesTabArgs = Pick<TabContext, "note" | "componentId"> & {
emptyListString?: string;
}
};
export default function InheritedAttributesTab({ note, componentId, emptyListString }: InheritedAttributesTabArgs) {
const [ inheritedAttributes, setInheritedAttributes ] = useState<FAttribute[]>();
@@ -23,10 +24,11 @@ export default function InheritedAttributesTab({ note, componentId, emptyListStr
attrs.sort((a, b) => {
if (a.noteId === b.noteId) {
return a.position - b.position;
} else {
// inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
return a.noteId < b.noteId ? -1 : 1;
}
// inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
return a.noteId < b.noteId ? -1 : 1;
});
setInheritedAttributes(attrs);
@@ -45,6 +47,7 @@ export default function InheritedAttributesTab({ note, componentId, emptyListStr
{inheritedAttributes?.length ? (
joinElements(inheritedAttributes.map(attribute => (
<InheritedAttribute
key={attribute.attributeId}
attribute={attribute}
onClick={(e) => {
setTimeout(
@@ -73,13 +76,13 @@ export default function InheritedAttributesTab({ note, componentId, emptyListStr
{attributeDetailWidgetEl}
</div>
)
);
}
function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onClick: (e: MouseEvent) => void }) {
const [ html, setHtml ] = useState<JQuery<HTMLElement> | string>("");
useEffect(() => {
attribute_renderer.renderAttribute(attribute, false).then(setHtml);
}, []);
}, [ attribute ]);
return (
<RawHtml

View File

@@ -70,7 +70,6 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) {
>
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />
<SwitchSplitOrientationButton {...innerProps} />
<ToggleReadOnlyButton {...innerProps} />
<SaveToNoteButton {...innerProps} />
@@ -230,15 +229,6 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
/>;
}
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript;env=");
return isEnabled && <NoteAction
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
/>;
}
function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
const helpUrl = getHelpUrlForNote(note);
const isEnabled = !!helpUrl;

View File

@@ -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<boolean>;
icon?: string;
}
export interface ButtonProperty {
type: "button",
label: string;
title?: string;
icon?: string;
onClick(context: BookContext): void;
}
export interface SplitButtonProperty extends Omit<ButtonProperty, "type"> {
type: "split-button";
items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode;
}
export interface NumberProperty {
type: "number",
label: string;
bindToLabel: FilterLabelsByType<number>;
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<string>;
/**
* 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<ViewTypeOptions, BookConfig> = {
grid: {
properties: []
@@ -156,6 +96,13 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
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 }) {
<FormDropdownDivider />
<ListExpandDepthButton label={t("book_properties.expand_all_levels")} depth="all" checked={currentDepth === "all"} {...context} />
</>
)
);
}
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 });
}
};
}

View File

@@ -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;
}

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