mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 19:05:59 +01:00
Compare commits
2 Commits
feat/impro
...
feature/el
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f6912cd57 | ||
|
|
d32dbf40f8 |
1
.github/workflows/checks.yml
vendored
1
.github/workflows/checks.yml
vendored
@@ -12,7 +12,6 @@ jobs:
|
||||
steps:
|
||||
- name: Check if PRs have conflicts
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
with:
|
||||
dirtyLabel: "merge-conflicts"
|
||||
repoToken: "${{ secrets.MERGE_CONFLICT_LABEL_PAT }}"
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -27,7 +27,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
nightly-electron:
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
if: github.repository == 'TriliumNext/Trilium'
|
||||
name: Deploy nightly
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
path: apps/desktop/upload
|
||||
|
||||
nightly-server:
|
||||
if: github.repository == ${{ vars.REPO_MAIN }}
|
||||
if: github.repository == 'TriliumNext/Trilium'
|
||||
name: Deploy server nightly
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
30
README.md
30
README.md
@@ -1,8 +1,8 @@
|
||||
# Trilium Notes
|
||||
|
||||
 
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://app.relative-ci.com/projects/Di5q7dz9daNDZ9UXi0Bp) [](https://hosted.weblate.org/engage/trilium/)
|
||||
|
||||
[English](./README.md) | [Chinese (Simplified)](./docs/README-ZH_CN.md) | [Chinese (Traditional)](./docs/README-ZH_TW.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
|
||||
@@ -46,15 +46,15 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
|
||||
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party themes, scripts, plugins and more.
|
||||
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
|
||||
|
||||
## ❓Why TriliumNext?
|
||||
## ⚠️ Why TriliumNext?
|
||||
|
||||
The original Trilium developer ([Zadam](https://github.com/zadam)) has graciously given the Trilium repository to the community project which resides at https://github.com/TriliumNext
|
||||
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
|
||||
|
||||
### ⬆️Migrating from Zadam/Trilium?
|
||||
### Migrating from Trilium?
|
||||
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Trilium instance. Simply [install TriliumNext/Trilium](#-installation) as usual and it will use your existing database.
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Simply [install TriliumNext/Notes](#-installation) as usual and it will use your existing database.
|
||||
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are compatible with the latest TriliumNext/Trilium version of [v0.63.7](https://github.com/TriliumNext/Trilium/releases/tag/v0.63.7). Any later versions of TriliumNext/Trilium have their sync versions incremented which prevents direct migration.
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
@@ -75,8 +75,8 @@ Feel free to join our official conversations. We would love to hear what feature
|
||||
|
||||
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
|
||||
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
|
||||
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For asynchronous discussions.)
|
||||
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug reports and feature requests.)
|
||||
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
|
||||
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
|
||||
|
||||
## 🏗 Installation
|
||||
|
||||
@@ -104,11 +104,9 @@ Currently only the latest versions of Chrome & Firefox are supported (and tested
|
||||
|
||||
To use TriliumNext on a mobile device, you can use a mobile web browser to access the mobile interface of a server installation (see below).
|
||||
|
||||
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more information on mobile app support.
|
||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid). Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
|
||||
|
||||
If you prefer a native Android app, you can use [TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
|
||||
Report bugs and missing features at [their repository](https://github.com/FliegendeWurst/TriliumDroid).
|
||||
Note: It is best to disable automatic updates on your server installation (see below) when using TriliumDroid since the sync version must match between Trilium and TriliumDroid.
|
||||
See issue https://github.com/TriliumNext/Notes/issues/72 for more information on mobile app support.
|
||||
|
||||
### Server
|
||||
|
||||
@@ -154,11 +152,11 @@ pnpm install
|
||||
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
|
||||
```
|
||||
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
|
||||
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
|
||||
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
|
||||
|
||||
## 👏 Shoutouts
|
||||
|
||||
@@ -170,7 +168,7 @@ Please view the [documentation guide](https://github.com/TriliumNext/Trilium/blo
|
||||
## 🤝 Support
|
||||
|
||||
Support for the TriliumNext organization will be possible in the near future. For now, you can:
|
||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/trilium/graphs/contributors))) for a full list)
|
||||
- Support continued development on TriliumNext by supporting our developers: [eliandoran](https://github.com/sponsors/eliandoran) (See the [repository insights]([developers]([url](https://github.com/TriliumNext/Notes/graphs/contributors))) for a full list)
|
||||
- Show a token of gratitude to the original Trilium developer ([zadam](https://github.com/sponsors/zadam)) via [PayPal](https://paypal.me/za4am) or Bitcoin (bitcoin:bc1qv3svjn40v89mnkre5vyvs2xw6y8phaltl385d2).
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ fi
|
||||
VERSION=$1
|
||||
SERIES=${VERSION:0:4}-latest
|
||||
|
||||
docker push TriliumNext/Trilium:$VERSION
|
||||
docker push TriliumNext/Trilium:$SERIES
|
||||
docker push zadam/trilium:$VERSION
|
||||
docker push zadam/trilium:$SERIES
|
||||
|
||||
if [[ $1 != *"beta"* ]]; then
|
||||
docker push TriliumNext/Trilium:latest
|
||||
docker push zadam/trilium:latest
|
||||
fi
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@playwright/test": "1.54.2",
|
||||
"@stylistic/eslint-plugin": "5.2.3",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/node": "22.17.2",
|
||||
"@types/node": "22.17.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"eslint": "9.33.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.98.0",
|
||||
"version": "0.97.2",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@fullcalendar/multimonth": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@maplibre/maplibre-gl-leaflet": "0.1.3",
|
||||
"@mermaid-js/layout-elk": "0.1.9",
|
||||
"@mermaid-js/layout-elk": "0.1.8",
|
||||
"@mind-elixir/node-menu": "5.0.0",
|
||||
"@popperjs/core": "2.11.8",
|
||||
"@triliumnext/ckeditor5": "workspace:*",
|
||||
@@ -36,7 +36,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.50.1",
|
||||
"globals": "16.3.0",
|
||||
"i18next": "25.3.6",
|
||||
"i18next": "25.3.4",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -46,13 +46,12 @@
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "16.2.0",
|
||||
"mermaid": "11.10.0",
|
||||
"mind-elixir": "5.0.6",
|
||||
"marked": "16.1.2",
|
||||
"mermaid": "11.9.0",
|
||||
"mind-elixir": "5.0.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.1",
|
||||
"react-i18next": "15.6.1",
|
||||
"preact": "10.27.0",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
|
||||
@@ -834,7 +834,7 @@ class FNote {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position < b.position ? -1 : 1;
|
||||
} else {
|
||||
// inherited promoted attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761
|
||||
// inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -78,7 +78,7 @@ export class WidgetsByParent {
|
||||
this.byParent[parentName]
|
||||
// previously, custom widgets were provided as a single instance, but that has the disadvantage
|
||||
// for splits where we actually need multiple instaces and thus having a class to instantiate is better
|
||||
// https://github.com/TriliumNext/Trilium/issues/4274
|
||||
// https://github.com/zadam/trilium/issues/4274
|
||||
.map((w: any) => (w.prototype ? new w() : w))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ async function copy(branchIds: string[]) {
|
||||
clipboardMode = "copy";
|
||||
|
||||
if (utils.isElectron()) {
|
||||
// https://github.com/TriliumNext/Trilium/issues/2401
|
||||
// https://github.com/zadam/trilium/issues/2401
|
||||
const { clipboard } = require("electron");
|
||||
const links: string[] = [];
|
||||
|
||||
|
||||
@@ -35,10 +35,8 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addOption(attributeEntity.name);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
processAttachment(loadResults, ec);
|
||||
} else if (ec.entityName === "blobs") {
|
||||
} else if (ec.entityName === "blobs" || ec.entityName === "etapi_tokens") {
|
||||
// NOOP - these entities are handled at the backend level and don't require frontend processing
|
||||
} else if (ec.entityName === "etapi_tokens") {
|
||||
loadResults.hasEtapiTokenChanges = true;
|
||||
} else {
|
||||
throw new Error(`Unknown entityName '${ec.entityName}'`);
|
||||
}
|
||||
@@ -79,7 +77,9 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
noteAttributeCache.invalidate();
|
||||
}
|
||||
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const appContext = (await import("../components/app_context.js")).default as any;
|
||||
await appContext.triggerEvent("entitiesReloaded", { loadResults });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import i18next from "i18next";
|
||||
import i18nextHttpBackend from "i18next-http-backend";
|
||||
import server from "./server.js";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
let locales: Locale[] | null;
|
||||
|
||||
@@ -17,7 +16,6 @@ export async function initLocale() {
|
||||
|
||||
locales = await server.get<Locale[]>("options/locales");
|
||||
|
||||
i18next.use(initReactI18next);
|
||||
await i18next.use(i18nextHttpBackend).init({
|
||||
lng: locale,
|
||||
fallbackLng: "en",
|
||||
|
||||
@@ -507,7 +507,7 @@ $(document).on("dblclick", "a", (e) => {
|
||||
$(document).on("mousedown", "a", (e) => {
|
||||
if (e.which === 2) {
|
||||
// prevent paste on middle click
|
||||
// https://github.com/TriliumNext/Trilium/issues/2995
|
||||
// https://github.com/zadam/trilium/issues/2995
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#preventing_default_actions
|
||||
e.preventDefault();
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AttachmentRow, EtapiTokenRow } from "@triliumnext/commons";
|
||||
import type { AttachmentRow } from "@triliumnext/commons";
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@@ -53,7 +53,6 @@ type EntityRowMappings = {
|
||||
options: OptionRow;
|
||||
revisions: RevisionRow;
|
||||
note_reordering: NoteReorderingRow;
|
||||
etapi_tokens: EtapiTokenRow;
|
||||
};
|
||||
|
||||
export type EntityRowNames = keyof EntityRowMappings;
|
||||
@@ -69,7 +68,6 @@ export default class LoadResults {
|
||||
private contentNoteIdToComponentId: ContentNoteIdToComponentIdRow[];
|
||||
private optionNames: string[];
|
||||
private attachmentRows: AttachmentRow[];
|
||||
public hasEtapiTokenChanges: boolean = false;
|
||||
|
||||
constructor(entityChanges: EntityChange[]) {
|
||||
const entities: Record<string, Record<string, any>> = {};
|
||||
@@ -217,8 +215,7 @@ export default class LoadResults {
|
||||
this.revisionRows.length === 0 &&
|
||||
this.contentNoteIdToComponentId.length === 0 &&
|
||||
this.optionNames.length === 0 &&
|
||||
this.attachmentRows.length === 0 &&
|
||||
!this.hasEtapiTokenChanges
|
||||
this.attachmentRows.length === 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async function mouseEnterHandler(this: HTMLElement) {
|
||||
if ($link.filter(":hover").length > 0) {
|
||||
$link.tooltip({
|
||||
container: "body",
|
||||
// https://github.com/TriliumNext/Trilium/issues/2794 https://github.com/TriliumNext/Trilium/issues/2988
|
||||
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
|
||||
// with bottom this flickering happens a bit less
|
||||
placement: "bottom",
|
||||
trigger: "manual",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { OptionNames } from "@triliumnext/commons";
|
||||
import server from "./server.js";
|
||||
import { isShare } from "./utils.js";
|
||||
|
||||
export type OptionValue = number | string;
|
||||
type OptionValue = number | string;
|
||||
|
||||
class Options {
|
||||
initializedPromise: Promise<void>;
|
||||
@@ -77,14 +76,6 @@ class Options {
|
||||
await server.put(`options`, payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves multiple options at once, by supplying a record where the keys are the option names and the values represent the stringified value to set.
|
||||
* @param newValues the record of keys and values.
|
||||
*/
|
||||
async saveMany<T extends OptionNames>(newValues: Record<T, OptionValue>) {
|
||||
await server.put<void>("options", newValues);
|
||||
}
|
||||
|
||||
async toggle(key: string) {
|
||||
await this.save(key, (!this.is(key)).toString());
|
||||
}
|
||||
|
||||
@@ -14,32 +14,6 @@ interface ShortcutBinding {
|
||||
// Store all active shortcut bindings for management
|
||||
const activeBindings: Map<string, ShortcutBinding[]> = new Map();
|
||||
|
||||
// Handle special key mappings and aliases
|
||||
const keyMap: { [key: string]: string[] } = {
|
||||
'return': ['Enter'],
|
||||
'enter': ['Enter'], // alias for return
|
||||
'del': ['Delete'],
|
||||
'delete': ['Delete'], // alias for del
|
||||
'esc': ['Escape'],
|
||||
'escape': ['Escape'], // alias for esc
|
||||
'space': [' ', 'Space'],
|
||||
'tab': ['Tab'],
|
||||
'backspace': ['Backspace'],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'pageup': ['PageUp'],
|
||||
'pagedown': ['PageDown'],
|
||||
'up': ['ArrowUp'],
|
||||
'down': ['ArrowDown'],
|
||||
'left': ['ArrowLeft'],
|
||||
'right': ['ArrowRight']
|
||||
};
|
||||
|
||||
// Function keys
|
||||
for (let i = 1; i <= 19; i++) {
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
function removeGlobalShortcut(namespace: string) {
|
||||
bindGlobalShortcut("", null, namespace);
|
||||
}
|
||||
@@ -150,6 +124,32 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle special key mappings and aliases
|
||||
const keyMap: { [key: string]: string[] } = {
|
||||
'return': ['Enter'],
|
||||
'enter': ['Enter'], // alias for return
|
||||
'del': ['Delete'],
|
||||
'delete': ['Delete'], // alias for del
|
||||
'esc': ['Escape'],
|
||||
'escape': ['Escape'], // alias for esc
|
||||
'space': [' ', 'Space'],
|
||||
'tab': ['Tab'],
|
||||
'backspace': ['Backspace'],
|
||||
'home': ['Home'],
|
||||
'end': ['End'],
|
||||
'pageup': ['PageUp'],
|
||||
'pagedown': ['PageDown'],
|
||||
'up': ['ArrowUp'],
|
||||
'down': ['ArrowDown'],
|
||||
'left': ['ArrowLeft'],
|
||||
'right': ['ArrowRight']
|
||||
};
|
||||
|
||||
// Function keys
|
||||
for (let i = 1; i <= 19; i++) {
|
||||
keyMap[`f${i}`] = [`F${i}`];
|
||||
}
|
||||
|
||||
const mappedKeys = keyMap[key.toLowerCase()];
|
||||
if (mappedKeys) {
|
||||
return mappedKeys.includes(e.key) || mappedKeys.includes(e.code);
|
||||
@@ -163,7 +163,7 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
|
||||
// For letter keys, use the physical key code for consistency
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
return e.code === `Key${key.toUpperCase()}`;
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -5,7 +5,7 @@ const SVG_MIME = "image/svg+xml";
|
||||
|
||||
export const isShare = !window.glob;
|
||||
|
||||
export function reloadFrontendApp(reason?: string) {
|
||||
function reloadFrontendApp(reason?: string) {
|
||||
if (reason) {
|
||||
logInfo(`Frontend app reload: ${reason}`);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export function reloadFrontendApp(reason?: string) {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
export function restartDesktopApp() {
|
||||
function restartDesktopApp() {
|
||||
if (!isElectron()) {
|
||||
reloadFrontendApp();
|
||||
return;
|
||||
@@ -125,7 +125,7 @@ function formatDateISO(date: Date) {
|
||||
return `${date.getFullYear()}-${padNum(date.getMonth() + 1)}-${padNum(date.getDate())}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
function formatDateTime(date: Date, userSuppliedFormat?: string): string {
|
||||
if (userSuppliedFormat?.trim()) {
|
||||
return dayjs(date).format(userSuppliedFormat);
|
||||
} else {
|
||||
@@ -144,7 +144,7 @@ function now() {
|
||||
/**
|
||||
* Returns `true` if the client is currently running under Electron, or `false` if running in a web browser.
|
||||
*/
|
||||
export function isElectron() {
|
||||
function isElectron() {
|
||||
return !!(window && window.process && window.process.type);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ function randomString(len: number) {
|
||||
return text;
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
function isMobile() {
|
||||
return (
|
||||
window.glob?.device === "mobile" ||
|
||||
// window.glob.device is not available in setup
|
||||
@@ -306,7 +306,7 @@ function copySelectionToClipboard() {
|
||||
}
|
||||
}
|
||||
|
||||
export function dynamicRequire(moduleName: string) {
|
||||
function dynamicRequire(moduleName: string) {
|
||||
if (typeof __non_webpack_require__ !== "undefined") {
|
||||
return __non_webpack_require__(moduleName);
|
||||
} else {
|
||||
@@ -374,17 +374,6 @@ async function openInAppHelp($button: JQuery<HTMLElement>) {
|
||||
|
||||
const inAppHelpPage = $button.attr("data-in-app-help");
|
||||
if (inAppHelpPage) {
|
||||
openInAppHelpFromUrl(inAppHelpPage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the in-app help at the given page in a split note. If there already is a split note open with a help page, it will be replaced by this one.
|
||||
*
|
||||
* @param inAppHelpPage the ID of the help note (excluding the `_help_` prefix).
|
||||
* @returns a promise that resolves once the help has been opened.
|
||||
*/
|
||||
export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
// Dynamic import to avoid import issues in tests.
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
@@ -410,6 +399,8 @@ export async function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
// There is already a help window open, make sure it opens on the right note.
|
||||
helpSubcontext.setNote(targetNote, { viewScope });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function initHelpButtons($el: JQuery<HTMLElement> | JQuery<Window>) {
|
||||
@@ -744,50 +735,6 @@ function isLaunchBarConfig(noteId: string) {
|
||||
return ["_lbRoot", "_lbAvailableLaunchers", "_lbVisibleLaunchers", "_lbMobileRoot", "_lbMobileAvailableLaunchers", "_lbMobileVisibleLaunchers"].includes(noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a class to the <body> of the page, where the class name is formed via a prefix and a value.
|
||||
* Useful for configurable options such as `heading-style-markdown`, where `heading-style` is the prefix and `markdown` is the dynamic value.
|
||||
* There is no separator between the prefix and the value, if needed it has to be supplied manually to the prefix.
|
||||
*
|
||||
* @param prefix the prefix.
|
||||
* @param value the value to be appended to the prefix.
|
||||
*/
|
||||
export function toggleBodyClass(prefix: string, value: string) {
|
||||
const $body = $("body");
|
||||
for (const clazz of Array.from($body[0].classList)) {
|
||||
// create copy to safely iterate over while removing classes
|
||||
if (clazz.startsWith(prefix)) {
|
||||
$body.removeClass(clazz);
|
||||
}
|
||||
}
|
||||
|
||||
$body.addClass(prefix + value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic comparison for equality between the two arrays. The values are strictly checked via `===`.
|
||||
*
|
||||
* @param a the first array to compare.
|
||||
* @param b the second array to compare.
|
||||
* @returns `true` if both arrays are equals, `false` otherwise.
|
||||
*/
|
||||
export function arrayEqual<T>(a: T[], b: T[]) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i=0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
restartDesktopApp,
|
||||
|
||||
@@ -79,7 +79,7 @@ body {
|
||||
height: unset !important;
|
||||
overflow: visible;
|
||||
position: unset;
|
||||
/* https://github.com/TriliumNext/Trilium/issues/3202 */
|
||||
/* https://github.com/zadam/trilium/issues/3202 */
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/TriliumNext/Trilium/issues/233 */
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
height: 100vh;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
@@ -139,15 +139,6 @@ textarea,
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
.form-group.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Add a gap between consecutive radios / check boxes */
|
||||
label.tn-radio + label.tn-radio,
|
||||
label.tn-checkbox + label.tn-checkbox {
|
||||
@@ -543,7 +534,7 @@ button.btn-sm {
|
||||
transform: translateX(7px);
|
||||
color: var(--muted-text-color);
|
||||
background-color: var(--main-background-color);
|
||||
/* Making this narrower because https://github.com/TriliumNext/Trilium/issues/502 (problem only in smaller font sizes) */
|
||||
/* Making this narrower because https://github.com/zadam/trilium/issues/502 (problem only in smaller font sizes) */
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
z-index: 1000;
|
||||
@@ -1117,7 +1108,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accented-background-color);
|
||||
display: flex; /* see https://github.com/TriliumNext/Trilium/issues/1590 */
|
||||
display: flex; /* see https://github.com/zadam/trilium/issues/1590 */
|
||||
}
|
||||
|
||||
.include-note.ck-placeholder::before {
|
||||
@@ -1251,7 +1242,7 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu {
|
||||
left: calc(100% - 2px); /* -2px, otherwise there's a small gap between menu and submenu where the hover can disappear */
|
||||
margin-top: -10px;
|
||||
min-width: 15rem;
|
||||
/* to make submenu scrollable https://github.com/TriliumNext/Trilium/issues/3136 */
|
||||
/* to make submenu scrollable https://github.com/zadam/trilium/issues/3136 */
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1747,12 +1738,16 @@ button.close:hover {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.options-section input[type="number"] {
|
||||
.options-number-input {
|
||||
/* overriding settings from .form-control */
|
||||
width: 10em !important;
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.options-mime-types {
|
||||
column-width: 250px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
@@ -182,6 +182,8 @@ div.note-detail-empty {
|
||||
|
||||
.options-section:not(.tn-no-card) {
|
||||
margin: auto;
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border-color) !important;
|
||||
box-shadow: var(--card-box-shadow);
|
||||
@@ -190,11 +192,6 @@ div.note-detail-empty {
|
||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||
}
|
||||
|
||||
body.desktop .option-section:not(.tn-no-card) {
|
||||
min-width: var(--options-card-min-width);
|
||||
max-width: var(--options-card-max-width);
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content.options {
|
||||
--default-padding: 15px;
|
||||
padding-top: calc(var(--default-padding) + var(--options-title-offset) + var(--options-title-font-size));
|
||||
@@ -236,6 +233,11 @@ body.desktop .option-section:not(.tn-no-card) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.options-section .options-mime-types {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.options-section .form-group {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -1165,7 +1165,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "笔记修订快照间隔",
|
||||
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <doc>wiki</doc>。",
|
||||
"note_revisions_snapshot_description": "笔记修订快照间隔是创建新笔记修订的时间。有关更多信息,请参见 <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"snapshot_time_interval_label": "笔记修订快照时间间隔:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1878,7 +1878,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "自定义日期/时间格式",
|
||||
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
||||
"description": "通过<kbd></kbd>或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a>。",
|
||||
"format_string": "日期/时间格式字符串:",
|
||||
"formatted_time": "格式化后日期/时间:"
|
||||
},
|
||||
@@ -1992,12 +1992,11 @@
|
||||
"help_title": "显示关于此画面的更多信息"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "新的 Trilium 主题已进入稳定版",
|
||||
"next_theme_message": "有一段时间,我们一直在设计新的主题,为了让应用程序看起来更加现代。",
|
||||
"next_theme_button": "切换至新的 Trilium 主题",
|
||||
"background_effects_title": "背景效果现已推出稳定版本",
|
||||
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
|
||||
"background_effects_button": "启用背景效果",
|
||||
"next_theme_title": "试用新 Trilium 主题",
|
||||
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
|
||||
"next_theme_button": "试用新主题",
|
||||
"dismiss": "关闭"
|
||||
"background_effects_button": "启用背景效果"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1138,7 +1138,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Snapshot-Intervall für Notizrevisionen",
|
||||
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <doc>Wiki</doc>.",
|
||||
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">Wiki</a>.",
|
||||
"snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
|
||||
@@ -1253,12 +1253,7 @@
|
||||
"selected_provider": "Selected Provider",
|
||||
"selected_provider_description": "Choose the AI provider for chat and completion features",
|
||||
"select_model": "Select model...",
|
||||
"select_provider": "Select provider...",
|
||||
"ai_enabled": "AI features enabled",
|
||||
"ai_disabled": "AI features disabled",
|
||||
"no_models_found_online": "No models found. Please check your API key and settings.",
|
||||
"no_models_found_ollama": "No Ollama models found. Please check if Ollama is running.",
|
||||
"error_fetching": "Error fetching models: {{error}}"
|
||||
"select_provider": "Select provider..."
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Zoom Factor (desktop build only)",
|
||||
@@ -1315,7 +1310,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <doc>wiki</doc> for more info.",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1377,7 +1372,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Custom Date/Time Format",
|
||||
"description": "Customize the format of the date and time inserted via <shortcut /> or the toolbar. See <doc>Day.js docs</doc> for available format tokens.",
|
||||
"description": "Customize the format of the date and time inserted via <kbd></kbd> or the toolbar. See <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a> for available format tokens.",
|
||||
"format_string": "Format string:",
|
||||
"formatted_time": "Formatted date/time:"
|
||||
},
|
||||
@@ -1999,22 +1994,11 @@
|
||||
"help_title": "Display more information about this screen"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "Try the new Trilium theme",
|
||||
"next_theme_message": "You are currently using the legacy theme, would you like to try the new theme?",
|
||||
"next_theme_button": "Try the new theme",
|
||||
"next_theme_title": "The new Trilium theme is now stable",
|
||||
"next_theme_message": "For a while now, we've been working on a new theme to give the application a more modern look.",
|
||||
"next_theme_button": "Switch to the new Trilium theme",
|
||||
"background_effects_title": "Background effects are now stable",
|
||||
"background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.",
|
||||
"background_effects_button": "Enable background effects",
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Related settings"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Color scheme for code blocks in text notes",
|
||||
"related_code_notes": "Color scheme for code notes"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
"background_effects_button": "Enable background effects"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,8 +157,7 @@
|
||||
"showSQLConsole": "mostrar consola SQL",
|
||||
"other": "Otro",
|
||||
"quickSearch": "centrarse en la entrada de búsqueda rápida",
|
||||
"inPageSearch": "búsqueda en la página",
|
||||
"title": "Hoja de ayuda"
|
||||
"inPageSearch": "búsqueda en la página"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar a nota",
|
||||
@@ -225,14 +224,11 @@
|
||||
"search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)",
|
||||
"modal_title": "Elija el tipo de nota",
|
||||
"modal_body": "Elija el tipo de nota/plantilla de la nueva nota:",
|
||||
"templates": "Plantillas",
|
||||
"builtin_templates": "Plantillas incluidas"
|
||||
"templates": "Plantillas"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "La contraseña no está establecida",
|
||||
"body1": "Las notas protegidas se cifran mediante una contraseña de usuario, pero la contraseña aún no se ha establecido.",
|
||||
"go_to_password_options": "Ir a opciones de contraseña",
|
||||
"body2": "Para poder proteger las notas, haz click en el botón inferior para abrir la pantalla de Opciones y establecer tu contraseña."
|
||||
"body1": "Las notas protegidas se cifran mediante una contraseña de usuario, pero la contraseña aún no se ha establecido."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Aviso",
|
||||
@@ -1108,10 +1104,7 @@
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
|
||||
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo.",
|
||||
"auto_theme": "Heredado (Sigue el esquema de colores del sistema)",
|
||||
"light_theme": "Heredado (Claro)",
|
||||
"dark_theme": "Heredado (Oscuro)"
|
||||
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo."
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "No iniciado",
|
||||
@@ -1310,7 +1303,7 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Intervalo de instantáneas de revisiones de notas",
|
||||
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo después de lo cual se creará una nueva revisión para la nota. Ver <doc>wiki</doc> para obtener más información.",
|
||||
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo después de lo cual se creará una nueva revisión para la nota. Ver <a href=\"https://triliumnext.github.io/docs/wiki/note-revisions.html\" class=\"external\"> wiki </a> para obtener más información.",
|
||||
"snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1372,7 +1365,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Formato de fecha/hora personalizada",
|
||||
"description": "Personalizar el formado de fecha y la hora insertada vía <shortcut /> o la barra de herramientas. Véa la <doc>documentación de Day.js</doc> para más tokens de formato disponibles.",
|
||||
"description": "Personalizar el formado de fecha y la hora insertada vía <kbd></kbd> o la barra de herramientas. Véa la <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">documentación de Day.js</a> para más tokens de formato disponibles.",
|
||||
"format_string": "Cadena de formato:",
|
||||
"formatted_time": "Fecha/hora personalizada:"
|
||||
},
|
||||
@@ -1990,16 +1983,6 @@
|
||||
"configure_launch_bar_description": "Abrir la configuración de la barra de inicio, para agregar o quitar elementos."
|
||||
},
|
||||
"modal": {
|
||||
"close": "Cerrar",
|
||||
"help_title": "Mostrar más información sobre esta pantalla"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "Prueba el nuevo tema de Trilium",
|
||||
"next_theme_message": "Estas usando actualmente el tema heredado, ¿Te gustaría probar el nuevo tema?",
|
||||
"next_theme_button": "Prueba el nuevo tema",
|
||||
"background_effects_title": "Los efectos de fondo son ahora estables",
|
||||
"background_effects_message": "En los dispositivos Windows, los efectos de fondo ya son totalmente estables. Los efectos de fondo añaden un toque de color a la interfaz de usuario difuminando el fondo que hay detrás. Esta técnica también se utiliza en otras aplicaciones como el Explorador de Windows.",
|
||||
"background_effects_button": "Activar efectos de fondo",
|
||||
"dismiss": "Desestimar"
|
||||
"close": "Cerrar"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "درباره Trilium Notes",
|
||||
"homepage": "صفحه اصلی:",
|
||||
"app_version": "نسخه برنامه:",
|
||||
"db_version": "نسخه پایگاه داده:",
|
||||
"sync_version": "نسخه منطبق:",
|
||||
"build_date": "تاریخ ساخت:",
|
||||
"build_revision": "نسخه بازنگری شده:",
|
||||
"data_directory": "دایرکتوری داده:"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "خطای بحرانی",
|
||||
"message": "خطای بحرانی رخ داده که مانع از اجرای برنامه می شود\n\n {{message}}\n\nبه احتمال زیاد ناشی از خطای غیرمنتظره در اجرای ناموفق یک اسکریپت است. برنامه را در مد ایمن اجرا کنید و خطا را بررسی نمایید."
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "افزودن لینک",
|
||||
"note": "یادداشت"
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Lisätietoja Trilium Notes:ista",
|
||||
"homepage": "Kotisivu:",
|
||||
"app_version": "Sovelluksen versio:",
|
||||
"db_version": "Tietokannan versio:",
|
||||
"build_date": "Koontipäivämäärä:",
|
||||
"data_directory": "Datakansio:",
|
||||
"sync_version": "Synkronoinnin versio:",
|
||||
"build_revision": "Sovelluksen versio:"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Kriittinen virhe"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Widgetin luonti epäonnistui"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Lisää linkki",
|
||||
"link_title": "Linkin otsikko",
|
||||
"button_add_link": "Lisää linkki",
|
||||
"note": "Muistio",
|
||||
"search_note": "etsi muistiota sen nimellä"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Etuliite: ",
|
||||
"save": "Tallenna"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Massatoiminnot",
|
||||
"available_actions": "Saatavilla olevat toiminnot",
|
||||
"chosen_actions": "Valitut toiminnot",
|
||||
"execute_bulk_actions": "Toteuta massatoiminnot",
|
||||
"bulk_actions_executed": "Massatoiminnot on toteutettu onnistuneesti.",
|
||||
"none_yet": "Ei vielä... lisää toiminto klikkaamalla jotiain yllä saatavilla olevaa yltä.",
|
||||
"labels": "Merkit",
|
||||
"relations": "Suhteet",
|
||||
"notes": "Muistiot",
|
||||
"other": "Muut",
|
||||
"affected_notes": "Vaikuttaa muistioihin"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Kopioi muistiot...",
|
||||
"help_on_links": "Apua linkkeihin",
|
||||
"notes_to_clone": "Kopioitavat muistiot",
|
||||
"target_parent_note": "Kohteen päämuistio",
|
||||
"search_for_note_by_its_name": "ensi muistiota sen nimellä",
|
||||
"cloned_note_prefix_title": "Kopioitu muistia näytetään puussa annetulla etuliitteellä",
|
||||
"prefix_optional": "Etuliite (valinnainen)",
|
||||
"clone_to_selected_note": "Kopioi valittuun muistioon",
|
||||
"note_cloned": "Muistio \"{{clonedTitle}}\" on kopioitu \"{{targetTitle}}\""
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Vahvistus",
|
||||
"cancel": "Peruuta",
|
||||
"ok": "OK",
|
||||
"also_delete_note": "Poista myös muistio"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "Poista muistion esikatselu",
|
||||
"close": "Sulje",
|
||||
"notes_to_be_deleted": "Seuraavat muistiot tullaan poistamaan ({{notesCount}})",
|
||||
"no_note_to_delete": "Muistioita ei poisteta (vain kopiot).",
|
||||
"cancel": "Peruuta",
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "Vie muistio",
|
||||
"close": "Sulje",
|
||||
"format_html": "HTML - suositeltu, sillä se säilyttää kaikki formatoinnit",
|
||||
"format_markdown": "Markdown - tämä säilyttää suurimman osan formatoinneista.",
|
||||
"opml_version_1": "OPML v1.0 - pelkkä teksti",
|
||||
"opml_version_2": "OPML v2.0 - sallii myös HTML:n",
|
||||
"export": "Vie",
|
||||
"choose_export_type": "Valitse ensin viennin tyyppi",
|
||||
"export_status": "Viennin tila",
|
||||
"export_in_progress": "Vienti käynnissä: {{progressCount}}",
|
||||
"export_finished_successfully": "Vienti valmistui onnistuneesti.",
|
||||
"format_pdf": "PDF - tulostukseen ja jakamiseen."
|
||||
},
|
||||
"help": {
|
||||
"title": "Lunttilappu",
|
||||
"noteNavigation": "Muistion navigointi",
|
||||
"goUpDown": "mene ylös/alas muistioiden listassa",
|
||||
"collapseExpand": "pienennä/suurenna solmu",
|
||||
"notSet": "ei asetettu",
|
||||
"goBackForwards": "mene taaksepäin/eteenpäin historiassa",
|
||||
"jumpToParentNote": "Hyppää ylempään muistioon",
|
||||
"collapseWholeTree": "pienennä koko muistio puu",
|
||||
"onlyInDesktop": "Vain työpöytänäkymässä (Electron build)",
|
||||
"openEmptyTab": "Avaa tyhjä välilehti",
|
||||
"closeActiveTab": "sulje aktiivinen välilehti",
|
||||
"activateNextTab": "aktivoi seuraava välilehti",
|
||||
"activatePreviousTab": "aktivoi edellinen välilehti",
|
||||
"creatingNotes": "Luo muistiota",
|
||||
"movingCloningNotes": "Siirrä / kopioi muistioita",
|
||||
"moveNoteUpHierarchy": "siirrä muistio ylöspäin listassa",
|
||||
"selectNote": "valitse muistio",
|
||||
"editingNotes": "Muokkaa solmua",
|
||||
"createEditLink": "luo / muokkaa ulkoista linkkiä",
|
||||
"createInternalLink": "luo sisäinen linkki",
|
||||
"insertDateTime": "lisää nykyinen päivämäärä ja aika hiiren kohdalle",
|
||||
"troubleshooting": "Vianmääritys",
|
||||
"reloadFrontend": "lataa Trilium:in käyttöliittymä",
|
||||
"showDevTools": "näytä kehittäjätyökalut",
|
||||
"showSQLConsole": "näytä SQL konsoli",
|
||||
"other": "Muut"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Tuo muistioon",
|
||||
"chooseImportFile": "Valitse tuonnin tiedosto",
|
||||
"options": "Valinnat",
|
||||
"safeImport": "Turvallinen tuonti",
|
||||
"shrinkImages": "Kutista kuvat",
|
||||
"replaceUnderscoresWithSpaces": "Korvaa alaviivat väleillä tuotujen muistioiden tiedostonimissä",
|
||||
"import": "Tuo",
|
||||
"failed": "Tuonti epäonnistui: {{message}}.",
|
||||
"html_import_tags": {
|
||||
"title": "HTML Tuonnin Tunnisteet",
|
||||
"placeholder": "Lisää HTML tunnisteet, yksi per rivi"
|
||||
},
|
||||
"import-status": "Tuonnin tila",
|
||||
"in-progress": "Tuonti vaiheessa: {{progress}}",
|
||||
"successful": "Tuonti valmistui onnistuneesti."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Sisällytä muistio",
|
||||
"label_note": "Muistio",
|
||||
"placeholder_search": "etsi muistiota sen nimellä",
|
||||
"box_size_small": "pieni (~ 10 riviä)",
|
||||
"box_size_medium": "keskisuuri (~ 30 riviä)",
|
||||
"button_include": "Sisällytä muistio"
|
||||
},
|
||||
"info": {
|
||||
"modalTitle": "Info viesti",
|
||||
"closeButton": "Sulje",
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Etsi koko tekstistä"
|
||||
},
|
||||
"call_to_action": {
|
||||
"dismiss": "Hylkää"
|
||||
}
|
||||
}
|
||||
@@ -202,8 +202,7 @@
|
||||
"okButton": "OK"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Rechercher dans le texte intégral",
|
||||
"search_placeholder": "Rechercher une note par son nom ou saisir ‘>’ pour les commandes…"
|
||||
"search_button": "Rechercher dans le texte intégral"
|
||||
},
|
||||
"markdown_import": {
|
||||
"dialog_title": "Importation Markdown",
|
||||
@@ -223,16 +222,11 @@
|
||||
"note_type_chooser": {
|
||||
"modal_title": "Choisissez le type de note",
|
||||
"modal_body": "Choisissez le type de note/le modèle de la nouvelle note :",
|
||||
"templates": "Modèles",
|
||||
"change_path_prompt": "Modifier l’emplacement de création de la nouvelle note :",
|
||||
"search_placeholder": "Rechercher le chemin par nom (par défaut si vide)",
|
||||
"builtin_templates": "Modèles intégrés"
|
||||
"templates": "Modèles"
|
||||
},
|
||||
"password_not_set": {
|
||||
"title": "Le mot de passe n'est pas défini",
|
||||
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini.",
|
||||
"body2": "Pour pouvoir protéger les notes, cliquez sur le bouton ci-dessous pour ouvrir la boîte de dialogue Options et définir votre mot de passe.",
|
||||
"go_to_password_options": "Accéder aux options de mot de passe"
|
||||
"body1": "Les notes protégées sont cryptées à l'aide d'un mot de passe utilisateur, mais le mot de passe n'a pas encore été défini."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Prompt",
|
||||
@@ -274,9 +268,7 @@
|
||||
"mime": "MIME : ",
|
||||
"file_size": "Taille du fichier :",
|
||||
"preview": "Aperçu :",
|
||||
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note.",
|
||||
"restore_button": "Restaurer",
|
||||
"delete_button": "Supprimer"
|
||||
"preview_not_available": "L'aperçu n'est pas disponible pour ce type de note."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Trier les enfants par...",
|
||||
@@ -426,8 +418,7 @@
|
||||
"other_notes_with_name": "Autres notes portant le nom {{attributeType}} \"{{attributeName}}\"",
|
||||
"and_more": "... et {{count}} plus.",
|
||||
"print_landscape": "Lors de l'exportation en PDF, change l'orientation de la page en paysage au lieu de portrait.",
|
||||
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>.",
|
||||
"color_type": "Couleur"
|
||||
"print_page_size": "Lors de l'exportation en PDF, change la taille de la page. Valeurs supportées : <code>A0</code>, <code>A1</code>, <code>A2</code>, <code>A3</code>, <code>A4</code>, <code>A5</code>, <code>A6</code>, <code>Legal</code>, <code>Letter</code>, <code>Tabloid</code>, <code>Ledger</code>."
|
||||
},
|
||||
"attribute_editor": {
|
||||
"help_text_body1": "Pour ajouter un label, tapez simplement par ex. <code>#rock</code>, ou si vous souhaitez également ajouter une valeur, tapez par ex. <code>#année = 2020</code>",
|
||||
@@ -586,8 +577,7 @@
|
||||
"september": "Septembre",
|
||||
"october": "Octobre",
|
||||
"november": "Novembre",
|
||||
"december": "Décembre",
|
||||
"cannot_find_week_note": "Impossible de trouver la note de la semaine"
|
||||
"december": "Décembre"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Fermer ce volet"
|
||||
@@ -731,8 +721,7 @@
|
||||
"basic_properties": {
|
||||
"note_type": "Type de note",
|
||||
"editable": "Modifiable",
|
||||
"basic_properties": "Propriétés de base",
|
||||
"language": "Langage"
|
||||
"basic_properties": "Propriétés de base"
|
||||
},
|
||||
"book_properties": {
|
||||
"view_type": "Type d'affichage",
|
||||
@@ -743,11 +732,7 @@
|
||||
"collapse": "Réduire",
|
||||
"expand": "Développer",
|
||||
"invalid_view_type": "Type de vue non valide '{{type}}'",
|
||||
"calendar": "Calendrier",
|
||||
"book_properties": "Propriétés de la collection",
|
||||
"table": "Tableau",
|
||||
"geo-map": "Carte géographique",
|
||||
"board": "Tableau de bord"
|
||||
"calendar": "Calendrier"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Aucune note modifiée ce jour-là...",
|
||||
@@ -824,8 +809,7 @@
|
||||
"unknown_label_type": "Type de label inconnu '{{type}}'",
|
||||
"unknown_attribute_type": "Type d'attribut inconnu '{{type}}'",
|
||||
"add_new_attribute": "Ajouter un nouvel attribut",
|
||||
"remove_this_attribute": "Supprimer cet attribut",
|
||||
"remove_color": "Supprimer l’étiquette de couleur"
|
||||
"remove_this_attribute": "Supprimer cet attribut"
|
||||
},
|
||||
"script_executor": {
|
||||
"query": "Requête",
|
||||
@@ -915,7 +899,7 @@
|
||||
"description1": "Le script de recherche permet de définir les résultats de la recherche en exécutant un script. Cela offre une flexibilité maximale lorsque la recherche standard ne suffit pas.",
|
||||
"description2": "Le script de recherche doit être de type \"code\" et sous-type \"backend JavaScript\". Le script doit retourner un tableau de noteIds ou de notes.",
|
||||
"example_title": "Voir cet exemple :",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"example_code": "// 1. préfiltrage à l'aide de la recherche standard\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. application de critères de recherche personnalisés\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "Notez que le script de recherche et la l'expression à rechercher standard ne peuvent pas être combinés."
|
||||
},
|
||||
"search_string": {
|
||||
@@ -1084,8 +1068,7 @@
|
||||
"max_width_label": "Largeur maximale du contenu en pixels",
|
||||
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
|
||||
"reload_button": "recharger l'interface",
|
||||
"reload_description": "changements par rapport aux options d'apparence",
|
||||
"max_width_unit": "Pixels"
|
||||
"reload_description": "changements par rapport aux options d'apparence"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barre de titre native (nécessite le redémarrage de l'application)",
|
||||
@@ -1108,10 +1091,7 @@
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-horizontal-title": "Horizontal",
|
||||
"layout-vertical-description": "la barre de raccourcis est à gauche (défaut)",
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur.",
|
||||
"auto_theme": "Hérité (suivre le schéma de couleurs du système)",
|
||||
"light_theme": "Hérité (clair)",
|
||||
"dark_theme": "Hérité (foncé)"
|
||||
"layout-horizontal-description": "la barre de raccourcis est sous la barre des onglets, cette-dernière est s'affiche en pleine largeur."
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "Facteur de zoom (version bureau uniquement)",
|
||||
@@ -1156,14 +1136,14 @@
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "Délai d'effacement des notes",
|
||||
"note_erasure_description": "Les notes supprimées (et les attributs, versions...) sont seulement marquées comme supprimées et il est possible de les récupérer à partir de la boîte de dialogue Notes récentes. Après un certain temps, les notes supprimées sont « effacées », ce qui signifie que leur contenu n'est plus récupérable. Ce paramètre vous permet de configurer la durée entre la suppression et l'effacement de la note.",
|
||||
"erase_notes_after": "Effacer les notes après :",
|
||||
"erase_notes_after": "Effacer les notes après:",
|
||||
"manual_erasing_description": "Vous pouvez également déclencher l'effacement manuellement (sans tenir compte de la durée définie ci-dessus) :",
|
||||
"erase_deleted_notes_now": "Effacer les notes supprimées maintenant",
|
||||
"deleted_notes_erased": "Les notes supprimées ont été effacées."
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Délai d'enregistrement automatique d'une version de note",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <doc>wiki</doc> pour plus d'informations.",
|
||||
"note_revisions_snapshot_description": "Le délai d'enregistrement automatique des versions de note définit le temps avant la création automatique d'une nouvelle version de note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
|
||||
"snapshot_time_interval_label": "Délai d'enregistrement automatique de version de note :"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
@@ -1670,16 +1650,5 @@
|
||||
},
|
||||
"modal": {
|
||||
"close": "Fermer"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Non démarré",
|
||||
"title": "Paramètres IA",
|
||||
"processed_notes": "Notes traitées",
|
||||
"n_notes_queued_0": "{{ count }} note en attente d’indexation",
|
||||
"n_notes_queued_1": "{{ count }} notes en attente d’indexation",
|
||||
"n_notes_queued_2": "",
|
||||
"notes_indexed_0": "{{ count }} note indexée",
|
||||
"notes_indexed_1": "{{ count }} notes indexées",
|
||||
"notes_indexed_2": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -35,11 +35,7 @@
|
||||
"link_title_arbitrary": "リンクタイトルは自由に変更可能"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "保存",
|
||||
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
||||
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
||||
"prefix": "接頭辞: ",
|
||||
"branch_prefix_saved": "ブランチ接頭辞が保存されました。"
|
||||
"save": "保存"
|
||||
},
|
||||
"global_menu": {
|
||||
"menu": "メニュー",
|
||||
@@ -63,10 +59,7 @@
|
||||
"zoom_out": "ズームアウト",
|
||||
"zoom_in": "ズームイン",
|
||||
"advanced": "高度",
|
||||
"toggle-zen-mode": "禅モード",
|
||||
"switch_to_mobile_version": "モバイル版に切り替え",
|
||||
"switch_to_desktop_version": "デスクトップ版に切り替え",
|
||||
"configure_launchbar": "ランチャーバーの設定"
|
||||
"toggle-zen-mode": "禅モード"
|
||||
},
|
||||
"left_pane_toggle": {
|
||||
"show_panel": "パネルを表示",
|
||||
@@ -89,17 +82,14 @@
|
||||
"help_on_links": "ヘルプへのリンク"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_all_clones_description": "すべてのクローンも削除(最近の変更では元に戻すことができる)",
|
||||
"delete_all_clones_description": "すべてのクローンも削除する(最近の変更では元に戻すことができる)",
|
||||
"erase_notes_description": "通常の(ソフト)削除では、ノートは削除されたものとしてマークされ、一定期間内に(最近の変更で)削除を取り消すことができます。このオプションをオンにすると、ノートは即座に削除され、削除を取り消すことはできません。",
|
||||
"erase_notes_warning": "すべてのクローンを含め、ノートを完全に消去します(元に戻せません)。これにより、アプリケーションは強制的にリロードされます。",
|
||||
"notes_to_be_deleted": "以下のノートが削除されます ({{notesCount}})",
|
||||
"no_note_to_delete": "ノートは削除されません(クローンのみ)。",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK",
|
||||
"close": "閉じる",
|
||||
"delete_notes_preview": "ノートのプレビューを削除",
|
||||
"broken_relations_to_be_deleted": "次のリレーション ({{relationCount}})は壊れているので消去されます",
|
||||
"deleted_relation_text": "削除予定のノート{{- note}}は{{- source}}からリレーション{{- relation}}によって参照されています."
|
||||
"close": "閉じる"
|
||||
},
|
||||
"calendar": {
|
||||
"mon": "月",
|
||||
@@ -151,15 +141,14 @@
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "タブを閉じる",
|
||||
"add_new_tab": "新しいタブを追加",
|
||||
"add_new_tab": "新しいタブ",
|
||||
"close": "閉じる",
|
||||
"close_other_tabs": "他のタブを閉じる",
|
||||
"close_right_tabs": "右側のタブをすべて閉じる",
|
||||
"close_all_tabs": "すべてのタブを閉じる",
|
||||
"reopen_last_tab": "最後に閉じたタブを開く",
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする",
|
||||
"new_tab": "新しいタブ"
|
||||
"move_tab_to_new_window": "このタブを新しいウィンドウに移動する",
|
||||
"copy_tab_to_new_window": "このタブを新しいウィンドウにコピーする"
|
||||
},
|
||||
"tasks": {
|
||||
"due": {
|
||||
@@ -209,10 +198,7 @@
|
||||
"confirm": {
|
||||
"confirmation": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"ok": "OK",
|
||||
"also_delete_note": "同時にノートを削除",
|
||||
"are_you_sure_remove_note": "本当に\"{{title}}\"をリレーションマップから除きたいですか? ",
|
||||
"if_you_dont_check": "これをチェックしないと、このノートはリレーションマップからのみ除かれます。"
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
@@ -279,12 +265,7 @@
|
||||
"showSQLConsole": "SQLコンソールを表示",
|
||||
"other": "その他",
|
||||
"quickSearch": "クイックサーチにフォーカス",
|
||||
"inPageSearch": "ページ内検索",
|
||||
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">「ジャンプ先」ダイアログ</a>を表示",
|
||||
"moveNoteUpDown": "ノートリストでノートを上/下に移動",
|
||||
"notSet": "未設定",
|
||||
"goUpDown": "ノートのリストで上下する",
|
||||
"editBranchPrefix": "アクティブノートのクローンの <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">プレフィックス</a> を編集する"
|
||||
"inPageSearch": "ページ内検索"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "ノートにインポート",
|
||||
@@ -359,9 +340,7 @@
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"zoom_out_title": "ズームアウト",
|
||||
"zoom_in_title": "ズームイン",
|
||||
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
|
||||
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
|
||||
"zoom_in_title": "ズームイン"
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"advanced": "高度",
|
||||
@@ -413,38 +392,19 @@
|
||||
"open_note_custom": "プログラムからノートを開く",
|
||||
"import_files": "ファイルをインポート",
|
||||
"export_note": "ノートをエクスポート",
|
||||
"delete_note": "ノートを削除",
|
||||
"delete_note": "ノートを削除する",
|
||||
"print_note": "ノートを印刷",
|
||||
"print_pdf": "PDFとしてエクスポート...",
|
||||
"convert_into_attachment": "添付ファイルに変換",
|
||||
"save_revision": "変更履歴を保存",
|
||||
"convert_into_attachment_failed": "ノート '{{title}}' の変換に失敗しました。",
|
||||
"convert_into_attachment_successful": "ノート '{{title}}' は添付ファイルに変換されました。",
|
||||
"convert_into_attachment_prompt": "本当にノート '{{title}}' を親ノートの添付ファイルに変換しますか?",
|
||||
"note_attachments": "ノートの添付ファイル"
|
||||
"print_pdf": "PDFとしてエクスポート..."
|
||||
},
|
||||
"command_palette": {
|
||||
"export_note_title": "ノートをエクスポート",
|
||||
"search_subtree_title": "サブツリー内を検索",
|
||||
"tree-action-name": "ツリー: {{name}}",
|
||||
"export_note_description": "現在のノートをエクスポート",
|
||||
"search_notes_title": "ノートを検索",
|
||||
"search_notes_description": "高度な検索を開く",
|
||||
"search_subtree_description": "現在のサブツリー内を検索",
|
||||
"search_history_title": "検索履歴を表示",
|
||||
"search_history_description": "過去の検索結果を見る"
|
||||
"search_subtree_title": "サブツリー内を検索"
|
||||
},
|
||||
"delete_note": {
|
||||
"delete_note": "ノートを削除"
|
||||
"delete_note": "ノートを削除する"
|
||||
},
|
||||
"board_view": {
|
||||
"delete-note": "ノートを削除",
|
||||
"insert-above": "上に挿入",
|
||||
"insert-below": "下に挿入",
|
||||
"delete-column": "列を削除",
|
||||
"delete-column-confirmation": "本当にこの列を削除しますか?対応する属性は、この列の下のノートでも削除されます。",
|
||||
"new-item": "新しいアイテム",
|
||||
"add-column": "列を追加"
|
||||
"delete-note": "ノートを削除する"
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "スクリプトを実行",
|
||||
@@ -522,8 +482,7 @@
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "ファイル",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
"title": "ファイル"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "ノート ID",
|
||||
@@ -533,54 +492,21 @@
|
||||
"note_size": "ノートサイズ",
|
||||
"calculate": "計算",
|
||||
"subtree_size": "(サブツリーサイズ: {{size}}、ノード数: {{count}})",
|
||||
"title": "ノート情報",
|
||||
"note_size_info": "ノートのサイズは、このノートに必要なストレージの概算を示します。これは、ノートの内容とそのノートの編集履歴の内容を考慮したものです。"
|
||||
"title": "ノート情報"
|
||||
},
|
||||
"image_properties": {
|
||||
"file_type": "ファイルタイプ",
|
||||
"file_size": "ファイルサイズ",
|
||||
"download": "ダウンロード",
|
||||
"open": "開く",
|
||||
"title": "画像",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
"title": "画像"
|
||||
},
|
||||
"revisions": {
|
||||
"download_button": "ダウンロード",
|
||||
"delete_button": "削除",
|
||||
"note_revisions": "ノートの変更履歴",
|
||||
"delete_all_revisions": "このノートの変更履歴をすべて削除",
|
||||
"delete_all_button": "変更履歴をすべて削除",
|
||||
"help_title": "変更履歴のヘルプ",
|
||||
"revision_last_edited": "この変更は{{date}}に行われました",
|
||||
"confirm_delete_all": "このノートのすべての変更履歴を削除しますか?",
|
||||
"no_revisions": "このノートに変更履歴はまだありません...",
|
||||
"restore_button": "復元",
|
||||
"confirm_restore": "この変更を復元しますか?現在のノートのタイトルとコンテンツはこの変更で上書きされます。",
|
||||
"confirm_delete": "この変更履歴を削除しますか?",
|
||||
"revisions_deleted": "ノートの変更履歴は削除されました。",
|
||||
"revision_restored": "ノートの変更が復元されました。",
|
||||
"revision_deleted": "ノートの変更履歴は削除されました。",
|
||||
"settings": "ノートの変更履歴の設定",
|
||||
"file_size": "ファイルサイズ:",
|
||||
"preview": "プレビュー:",
|
||||
"preview_not_available": "このノートタイプではプレビューは利用できません。"
|
||||
"delete_button": "削除"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"download": "ダウンロード",
|
||||
"open_externally": "外部で開く",
|
||||
"open_externally_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"open_custom": "プログラムから開く",
|
||||
"open_custom_title": "ファイルを外部アプリケーションで開き、変更を監視します。その後、変更されたバージョンをTriliumにアップロードできるようになります。",
|
||||
"rename_attachment": "添付ファイルの名前を変更",
|
||||
"copy_link_to_clipboard": "リンクをクリップボードにコピー",
|
||||
"convert_attachment_into_note": "添付ファイルをノートに変換",
|
||||
"delete_attachment": "添付ファイルを削除",
|
||||
"open_externally_detail_page": "添付ファイルを外部で開くは、詳細ページからのみ可能です。まず添付ファイル詳細をクリックしてから、再度操作を行ってください。",
|
||||
"open_custom_client_only": "添付ファイルをプログラムから開くは、デスクトップクライアントからのみ可能です。",
|
||||
"delete_confirm": "本当に添付ファイル '{{title}}' を削除しますか?",
|
||||
"delete_success": "添付ファイル '{{title}}' は削除されました。",
|
||||
"enter_new_name": "新しい添付ファイルの名前を入力してください",
|
||||
"upload_new_revision": "編集履歴をアップロード"
|
||||
"download": "ダウンロード"
|
||||
},
|
||||
"etapi": {
|
||||
"created": "作成日時",
|
||||
@@ -652,41 +578,15 @@
|
||||
"parent_count": "クローンの数",
|
||||
"random": "ランダムな順番",
|
||||
"asc": "昇順(デフォルト)",
|
||||
"desc": "降順",
|
||||
"content_and_attachments_size": "添付ファイルを含むノート内容のサイズ",
|
||||
"content_and_attachments_and_revisions_size": "添付ファイルと編集履歴を含むノート内容のサイズ",
|
||||
"revision_count": "編集履歴の回数",
|
||||
"owned_label_count": "ラベルの数",
|
||||
"owned_relation_count": "関係の数"
|
||||
"desc": "降順"
|
||||
},
|
||||
"table_view": {
|
||||
"sort-column-descending": "降順",
|
||||
"sort-column-ascending": "昇順",
|
||||
"new-row": "新しい行",
|
||||
"new-column": "新しい列",
|
||||
"sort-column-by": "\"{{title}}\" で並べ替え",
|
||||
"sort-column-clear": "並べ替えをクリア",
|
||||
"hide-column": "列 \"{{title}}\" を隠す",
|
||||
"show-hide-columns": "列を表示/非表示",
|
||||
"row-insert-above": "上に行を挿入",
|
||||
"row-insert-below": "下に行を挿入",
|
||||
"row-insert-child": "子ノートを挿入",
|
||||
"add-column-to-the-left": "列を左に追加",
|
||||
"add-column-to-the-right": "列を右に追加",
|
||||
"edit-column": "列を編集",
|
||||
"delete_column_confirmation": "この列を本当に削除してもよろしいですか?対応する属性はすべてのノートから削除されます。",
|
||||
"delete-column": "列を削除",
|
||||
"new-column-label": "ラベル",
|
||||
"new-column-relation": "関係"
|
||||
"sort-column-ascending": "昇順"
|
||||
},
|
||||
"search_script": {
|
||||
"title": "検索スクリプト:",
|
||||
"placeholder": "ノート名で検索",
|
||||
"description1": "検索スクリプトは、スクリプトを実行することによって検索結果を定義することができます。標準の検索では不十分な場合に、最大限の柔軟性を提供します。",
|
||||
"description2": "検索スクリプトはノートタイプが \"code\" かつ \"JavaScript backend\" でなければならない。スクリプトは、 noteIds または note の配列を返す必要があります。",
|
||||
"example_title": "例は以下です:",
|
||||
"example_code": "// 1. 標準検索によるプレフィルタリング\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. カスタム検索条件の適用\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "検索スクリプトと文字列検索は、互いに組み合わせることはできません。"
|
||||
"placeholder": "ノート名で検索"
|
||||
},
|
||||
"include_note": {
|
||||
"placeholder_search": "ノート名で検索",
|
||||
@@ -734,12 +634,7 @@
|
||||
"system-default": "システムのデフォルト"
|
||||
},
|
||||
"max_content_width": {
|
||||
"reload_button": "フロントエンドをリロード",
|
||||
"title": "コンテンツ幅",
|
||||
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
||||
"max_width_label": "最大コンテンツ幅",
|
||||
"max_width_unit": "ピクセル",
|
||||
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください"
|
||||
"reload_button": "フロントエンドをリロード"
|
||||
},
|
||||
"theme": {
|
||||
"title": "アプリのテーマ",
|
||||
@@ -818,8 +713,7 @@
|
||||
"shortcut_info": "オプション -> ショートカット('右ペイン切り替え')で、右ペイン(目次を含む)を素早く切り替えるキーボードショートカットを設定できます。"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "目次",
|
||||
"options": "オプション"
|
||||
"table_of_contents": "目次"
|
||||
},
|
||||
"text_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
@@ -830,12 +724,11 @@
|
||||
"code_auto_read_only_size": {
|
||||
"title": "自動読み取り専用のサイズ",
|
||||
"description": "自動読み取り専用のノートサイズは、ノートが読み取り専用モード(パフォーマンス上の理由)で表示されるようになるサイズです。",
|
||||
"unit": "文字",
|
||||
"label": "自動読み取り専用のサイズ(コードノート)"
|
||||
"unit": "文字"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "日付/時刻フォーマットのカスタム",
|
||||
"description": "<shortcut />またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <doc>Day.js ドキュメント</doc> を参照してください。",
|
||||
"description": "<kbd></kbd>またはツールバーから挿入される日付と時刻のフォーマットをカスタマイズする。 利用可能なトークンについては <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js ドキュメント</a> を参照してください。",
|
||||
"format_string": "文字列形式:",
|
||||
"formatted_time": "日付/時刻形式:"
|
||||
},
|
||||
@@ -911,8 +804,7 @@
|
||||
"close_label": "閉じる"
|
||||
},
|
||||
"modal": {
|
||||
"close": "閉じる",
|
||||
"help_title": "この画面に関する詳細情報を表示"
|
||||
"close": "閉じる"
|
||||
},
|
||||
"bookmark_switch": {
|
||||
"bookmark": "ブックマーク",
|
||||
@@ -920,39 +812,10 @@
|
||||
"remove_bookmark": "ブックマークを削除"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"delete": "削除",
|
||||
"close_button_title": "変更を破棄して閉じる",
|
||||
"attr_is_owned_by": "属性の所有者",
|
||||
"attr_name_title": "属性の名前は英数字、コロン、アンダーバーのみで構成します",
|
||||
"name": "名前",
|
||||
"value": "値",
|
||||
"promoted_alias": "エイリアス",
|
||||
"multiplicity_title": "多重性とは、同じ名前を持つ属性をいくつ作成できるかを定義するもので、1つまたはそれ以上です。",
|
||||
"multiplicity": "多重性",
|
||||
"single_value": "単一の値",
|
||||
"multi_value": "複数の値",
|
||||
"label_type_title": "ラベルのタイプは、Triliumがラベルの値を入力するのに適したインターフェースを選択するのに役立ちます。",
|
||||
"label_type": "タイプ",
|
||||
"text": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値",
|
||||
"date": "日付",
|
||||
"date_time": "日付と時間",
|
||||
"time": "時間",
|
||||
"url": "URL",
|
||||
"save_and_close": "保存して閉じる <kbd>Ctrl+Enter</kbd>",
|
||||
"precision_title": "値設定インターフェースで、小数点以下何桁まで利用可能にするか。",
|
||||
"precision": "精度",
|
||||
"digits": "桁",
|
||||
"inheritable_title": "継承属性は、このツリー配下のすべての子孫に継承されます。",
|
||||
"inheritable": "継承",
|
||||
"related_notes_title": "このラベルが付いた他のノート"
|
||||
"delete": "削除"
|
||||
},
|
||||
"link_context_menu": {
|
||||
"open_note_in_popup": "クイックエディット",
|
||||
"open_note_in_new_tab": "新しいタブでノートを開く",
|
||||
"open_note_in_new_split": "新しく分割してノートを開く",
|
||||
"open_note_in_new_window": "新しいウィンドウでノートを開く"
|
||||
"open_note_in_popup": "クイックエディット"
|
||||
},
|
||||
"note_tooltip": {
|
||||
"quick-edit": "クイックエディット"
|
||||
@@ -998,400 +861,6 @@
|
||||
"hide-archived-notes": "アーカイブノートを隠す",
|
||||
"automatically-collapse-notes": "ノートを自動的に折りたたむ",
|
||||
"automatically-collapse-notes-title": "一定期間使用されないと、ツリーを整理するためにノートは折りたたまれます。",
|
||||
"save-changes": "変更を保存して適用",
|
||||
"auto-collapsing-notes-after-inactivity": "非アクティブ状態が続いたためノートが自動で折りたたまれます...",
|
||||
"create-child-note": "子ノートを作成"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
"affected_notes": "影響されたノート",
|
||||
"include_descendants": "選択したノートの子ノートを含む",
|
||||
"available_actions": "利用可能なアクション",
|
||||
"chosen_actions": "選択されたアクション",
|
||||
"execute_bulk_actions": "一括操作を実行",
|
||||
"bulk_actions_executed": "一括操作が成功に実行されました。",
|
||||
"labels": "ラベル",
|
||||
"relations": "関係",
|
||||
"notes": "ノート",
|
||||
"other": "その他",
|
||||
"none_yet": "アクションを上のリストからクリックして追加。"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "ここにノートのタイトルを入力..."
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "指定された検索パラメータに該当するノートは見つかりませんでした。",
|
||||
"search_not_executed": "検索はまだ実行されていません。上の「検索」ボタンをクリックすると、検索結果が表示されます。"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "このクエリでは行が返されませんでした"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "テーブル"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "保存が完了するまで数秒お待ちください。その後、もう一度お試しいただけます。"
|
||||
},
|
||||
"note_create": {
|
||||
"duplicated": "ノート \"{{title}}\" は複製されました。"
|
||||
},
|
||||
"clipboard": {
|
||||
"cut": "ノートはクリップボードにカットされました。",
|
||||
"copied": "ノートはクリップボードにコピーされました。",
|
||||
"copy_failed": "権限の問題で、クリップボードにコピーできません。",
|
||||
"copy_success": "クリップボードにコピーしました。"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"add-note-launcher": "ノートランチャーを追加",
|
||||
"add-script-launcher": "スクリプトランチャーを追加",
|
||||
"add-custom-widget": "カスタムウィジェットを追加",
|
||||
"add-spacer": "スペーサーを追加",
|
||||
"delete": "削除 <kbd data-command=\"deleteNotes\"></kbd>",
|
||||
"reset": "リセット",
|
||||
"move-to-visible-launchers": "可視ランチャーに移動",
|
||||
"move-to-available-launchers": "利用可能なランチャーに移動",
|
||||
"duplicate-launcher": "ランチャーの複製 <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "自動検出"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "コードブロック",
|
||||
"description": "テキストノート内のコードブロックのシンタックスハイライトを制御します。コードノートには影響しません。",
|
||||
"color-scheme": "カラースキーム"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "単語の折り返し",
|
||||
"theme_none": "シンタックスハイライトなし",
|
||||
"theme_group_light": "ライトテーマ",
|
||||
"theme_group_dark": "ダークテーマ",
|
||||
"copy_title": "クリップボードにコピー"
|
||||
},
|
||||
"editor": {
|
||||
"title": "エディター"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "書式設定ツールバー",
|
||||
"floating": {
|
||||
"description": "編集ツールがカーソル付近に表示されます;"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "固定",
|
||||
"description": "編集ツールは「書式設定」のリボンタブに表示されます。"
|
||||
},
|
||||
"multiline-toolbar": "ツールバーが収まりきらない場合は、複数行で表示する。"
|
||||
}
|
||||
},
|
||||
"electron_context_menu": {
|
||||
"add-term-to-dictionary": "辞書に \"{{term}}\" を追加",
|
||||
"cut": "カット",
|
||||
"copy": "コピー",
|
||||
"copy-link": "リンクをコピー",
|
||||
"paste": "ペースト",
|
||||
"paste-as-plain-text": "プレーンテキストで貼り付け",
|
||||
"search_online": "{{searchEngine}} で \"{{term}}\" を検索"
|
||||
},
|
||||
"duration": {
|
||||
"seconds": "秒",
|
||||
"minutes": "分",
|
||||
"hours": "時間",
|
||||
"days": "日"
|
||||
},
|
||||
"share": {
|
||||
"title": "共有設定",
|
||||
"redirect_bare_domain": "ネイキッドドメインを共有ページにリダイレクト",
|
||||
"redirect_bare_domain_description": "匿名のユーザーをログイン画面ではなく共有ページにリダイレクトします",
|
||||
"show_login_link_description": "共有ページの下部にログインリンクを追加",
|
||||
"share_root_found": "共有ルートノート '{{noteTitle}}' の準備が完了",
|
||||
"share_root_not_found": "#shareRoot のラベルが付いたノートが見つかりません",
|
||||
"share_root_not_shared": "ノート '{{noteTitle}}' は #shareRoot のラベルを持っていますが、共有されていません"
|
||||
},
|
||||
"time_selector": {
|
||||
"invalid_input": "入力された時間値が有効な数値ではありません。",
|
||||
"minimum_input": "入力された時間値は {{minimumSeconds}} 秒以上である必要があります。"
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "未設定",
|
||||
"configure-languages": "言語を設定..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "コンテンツの言語",
|
||||
"description": "読み取り専用または編集可能なテキストノートの基本プロパティセクションの言語選択に表示する言語を 1 つ以上選択します。これにより、スペルチェックや右から左へのサポートなどの機能が利用できるようになります。"
|
||||
},
|
||||
"png_export_button": {
|
||||
"button_title": "図をPNG形式でエクスポート"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "図をPNG形式でエクスポートできませんでした。"
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "外観",
|
||||
"word_wrapping": "単語の折り返し",
|
||||
"color-scheme": "カラースキーム"
|
||||
},
|
||||
"cpu_arch_warning": {
|
||||
"title": "ARM64版をダウンロードしてください",
|
||||
"dont_show_again": "この警告を二度と表示しない",
|
||||
"continue_anyway": "とにかく続ける"
|
||||
},
|
||||
"editorfeatures": {
|
||||
"emoji_completion_enabled": "絵文字のオートコンプリートを有効",
|
||||
"note_completion_enabled": "ノートのオートコンプリートを有効"
|
||||
},
|
||||
"table_context_menu": {
|
||||
"delete_row": "行を削除"
|
||||
},
|
||||
"content_renderer": {
|
||||
"open_externally": "外部で開く"
|
||||
},
|
||||
"ai_llm": {
|
||||
"title": "AI 設定",
|
||||
"enable_ai_features": "AI/LLM 機能を有効化",
|
||||
"enable_ai_description": "ノートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
|
||||
"openai_tab": "OpenAI",
|
||||
"anthropic_tab": "Anthropic",
|
||||
"voyage_tab": "Voyage AI",
|
||||
"ollama_tab": "Ollama",
|
||||
"enable_ai": "AI/LLM 機能を有効化",
|
||||
"enable_ai_desc": "ノートの要約、コンテンツ生成、その他のLLM機能などのAI機能を有効にする",
|
||||
"provider_configuration": "AI プロバイダーの設定",
|
||||
"provider_precedence": "プロバイダーの優先順位",
|
||||
"provider_precedence_description": "カンマで区切られたプロバイダーの優先順位リスト(例: 'openai,anthropic,ollama')",
|
||||
"temperature_description": "応答のランダム性を制御する(0 = 決定的、2 = 最大ランダム性)",
|
||||
"system_prompt_description": "すべてのAIとの対話に使用されるデフォルトのシステムプロンプト",
|
||||
"system_prompt": "システムプロンプト",
|
||||
"openai_configuration": "OpenAIの設定",
|
||||
"openai_settings": "OpenAIの設定",
|
||||
"api_key": "APIキー",
|
||||
"model": "モデル",
|
||||
"openai_api_key_description": "OpenAIのAIサービスにアクセスするためのAPIキー",
|
||||
"anthropic_api_key_description": "AnthropicのClaudeモデルにアクセスするためのAPIキー",
|
||||
"default_model": "デフォルトモデル",
|
||||
"openai_model_description": "例: gpt-4o, gpt-4-turbo, gpt-3.5-turbo",
|
||||
"openai_url_description": "デフォルト: https://api.openai.com/v1",
|
||||
"anthropic_settings": "Anthropicの設定",
|
||||
"anthropic_model_description": "チャット補完用のAnthropic Claudeモデル",
|
||||
"voyage_settings": "Voyage AIの設定",
|
||||
"ollama_settings": "Ollamaの設定",
|
||||
"ollama_url_description": "Ollama API の URL(デフォルト: http://localhost:11434)",
|
||||
"anthropic_url_description": "Anthropic APIのベースURL(デフォルト: https://api.anthropic.com)",
|
||||
"ollama_model_description": "チャット補完用のOllamaモデル",
|
||||
"anthropic_configuration": "Anthropicの設定",
|
||||
"voyage_configuration": "Voyage AIの設定",
|
||||
"voyage_url_description": "デフォルト: https://api.voyageai.com/v1",
|
||||
"ollama_configuration": "Ollamaの設定",
|
||||
"enable_ollama": "Ollamaを有効",
|
||||
"enable_ollama_description": "ローカルAIモデルを利用するためOllamaを有効にする",
|
||||
"ollama_url": "Ollama URL",
|
||||
"ollama_model": "Ollamaモデル",
|
||||
"refresh_models": "モデルを更新",
|
||||
"refreshing_models": "更新中...",
|
||||
"error": "エラー",
|
||||
"retry": "再試行",
|
||||
"partial": "{{ percentage }}%完了",
|
||||
"processing": "処理中({{percentage}}%)",
|
||||
"complete": "完了(100%)",
|
||||
"refreshing": "更新中...",
|
||||
"auto_refresh_notice": "{{seconds}}秒ごとに自動更新",
|
||||
"ai_settings": "AI 設定",
|
||||
"api_key_tooltip": "サービスにアクセスするためのAPIキー",
|
||||
"empty_key_warning": {
|
||||
"anthropic": "Anthropic APIキーが空です。有効な APIキーを入力してください。",
|
||||
"openai": "OpenAI APIキーが空です。有効なAPIキーを入力してください。",
|
||||
"voyage": "Voyage APIキーが空です。有効なAPIキーを入力してください。",
|
||||
"ollama": "Ollama APIキーが空です。有効なAPIキーを入力してください。"
|
||||
},
|
||||
"agent": {
|
||||
"processing": "処理中...",
|
||||
"loading": "読み込み中...",
|
||||
"generating": "生成中..."
|
||||
},
|
||||
"name": "AI",
|
||||
"openai": "OpenAI",
|
||||
"error_contacting_provider": "AIプロバイダーへの接続中にエラーが発生しました。設定とインターネット接続をご確認ください。",
|
||||
"ollama_no_url": "Ollamaは設定されていません。有効なURLを入力してください。",
|
||||
"chat": {
|
||||
"root_note_title": "AIチャット",
|
||||
"root_note_content": "このメモには、保存されたAIチャットの会話が含まれています。",
|
||||
"new_chat_title": "新しいチャット",
|
||||
"create_new_ai_chat": "新しいAIチャットを作成"
|
||||
},
|
||||
"create_new_ai_chat": "新しいAIチャットを作成",
|
||||
"configuration_warnings": "AIの設定に問題があります。設定を確認してください。",
|
||||
"experimental_warning": "LLM機能は現在実験的です - ご注意ください。",
|
||||
"selected_provider_description": "チャットおよび補完機能のAIプロバイダーを選択する",
|
||||
"selected_provider": "プロバイダーを選択",
|
||||
"select_model": "モデルを選択...",
|
||||
"select_provider": "プロバイダーを選択..."
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "ラベルを追加",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
|
||||
"new_value_placeholder": "新しい値"
|
||||
},
|
||||
"delete_label": {
|
||||
"delete_label": "ラベルを削除",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "ラベル名の変更",
|
||||
"rename_label_from": "ラベル名を",
|
||||
"old_name_placeholder": "古い名前",
|
||||
"to": "から",
|
||||
"new_name_placeholder": "新しい名前",
|
||||
"name_title": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"update_label_value": {
|
||||
"update_label_value": "ラベルの値を更新",
|
||||
"label_name_placeholder": "ラベル名",
|
||||
"label_name_title": "英数字、アンダーバー、コロンが使用可能な文字です。",
|
||||
"new_value_placeholder": "新しい値",
|
||||
"help_text_note": "このメソッドは値なしで呼び出すこともできます。その場合、値なしでラベルがノートに割り当てられます。"
|
||||
},
|
||||
"add_relation": {
|
||||
"add_relation": "関係を追加",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"delete_relation": {
|
||||
"delete_relation": "関係を削除",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"rename_relation": {
|
||||
"rename_relation": "関係の名前を変更",
|
||||
"rename_relation_from": "関係の名前を",
|
||||
"old_name": "古い名前",
|
||||
"to": "から",
|
||||
"new_name": "新しい名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"update_relation_target": {
|
||||
"update_relation": "関係の更新",
|
||||
"relation_name": "関係の名前",
|
||||
"allowed_characters": "英数字、アンダーバー、コロンが使用可能な文字です。"
|
||||
},
|
||||
"revisions_button": {
|
||||
"note_revisions": "ノートの変更履歴"
|
||||
},
|
||||
"note_launcher": {
|
||||
"this_launcher_doesnt_define_target_note": "このランチャーはターゲットノートを定義していません。"
|
||||
},
|
||||
"zpetne_odkazy": {
|
||||
"backlink": "{{count}} バックリンク",
|
||||
"backlinks": "{{count}} バックリンク",
|
||||
"relation": "関係"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"delete_this_note": "このノートを削除",
|
||||
"error_unrecognized_command": "認識されないコマンド {{command}}"
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "継承属性",
|
||||
"no_inherited_attributes": "継承属性ではありません。"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "拡大表示",
|
||||
"collapse": "通常サイズに折りたたむ",
|
||||
"title": "ノートマップ",
|
||||
"link-distance": "リンク距離"
|
||||
},
|
||||
"owned_attribute_list": {
|
||||
"owned_attributes": "所有属性"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"unset-field-placeholder": "未設定",
|
||||
"open_external_link": "外部でリンクを開く",
|
||||
"unknown_label_type": "不明なラベルタイプ '{{type}}'",
|
||||
"unknown_attribute_type": "不明な属性タイプ '{{type}}'",
|
||||
"add_new_attribute": "新しい属性を追加",
|
||||
"remove_this_attribute": "この属性を削除",
|
||||
"remove_color": "このカラーラベルを削除"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "新しいタブで開く",
|
||||
"remove_note": "ノートを削除",
|
||||
"edit_title": "タイトルを編集",
|
||||
"rename_note": "ノート名を変更",
|
||||
"enter_new_title": "新しいノート名を入力:",
|
||||
"remove_relation": "関係を削除",
|
||||
"confirm_remove_relation": "本当にこの関係を削除しますか?",
|
||||
"specify_new_relation_name": "新しい関係の名前(使用可能な文字: 英数字、コロン、アンダースコア)を指定:",
|
||||
"note_not_found": "ノート {{noteId}} が見つかりません!",
|
||||
"enter_title_of_new_note": "新しいノートのタイトルを入力",
|
||||
"default_new_note_title": "新しいノート"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "データベースの匿名化",
|
||||
"full_anonymization": "完全匿名化",
|
||||
"full_anonymization_description": "この操作により、データベースの新しいコピーが作成され、匿名化されます(すべてのノートの内容を削除し、構造と一部の非機密メタデータのみを残します)。これにより、個人データが漏洩する心配なく、デバッグ目的でオンライン共有できます。",
|
||||
"save_fully_anonymized_database": "完全に匿名化されたデータベースを保存",
|
||||
"light_anonymization": "軽い匿名化",
|
||||
"light_anonymization_description": "この操作により、データベースの新しいコピーが作成され、軽い匿名化が適用されます。具体的には、すべてのノートの内容のみが削除され、タイトルと属性はそのまま残ります。さらに、カスタムJSフロントエンド/バックエンドスクリプトノートとカスタムウィジェットもそのまま残ります。これにより、問題のデバッグのためのコンテキストがより多く提供されます。",
|
||||
"choose_anonymization": "完全に匿名化したデータベースを提供するか、軽く匿名化したデータベースを提供するかは、あなた自身が決めることができます。完全に匿名化されたDBであっても非常に有用ですが、場合によっては軽く匿名化されたDBの方がバグの特定と修正のプロセスを速めることができます。",
|
||||
"save_lightly_anonymized_database": "軽く匿名化されたデータベースを保存",
|
||||
"existing_anonymized_databases": "既存の匿名化データベース",
|
||||
"creating_fully_anonymized_database": "完全に匿名化されたデータベースを作成中...",
|
||||
"creating_lightly_anonymized_database": "軽く匿名化されたデータベースを作成中...",
|
||||
"error_creating_anonymized_database": "匿名化データベースの作成に失敗しました。詳細はバックエンドログを確認してください",
|
||||
"successfully_created_fully_anonymized_database": "完全に匿名化されたデータベースを {{anonymizedFilePath}} に作成",
|
||||
"successfully_created_lightly_anonymized_database": "軽く匿名化されたデータベースを {{anonymizedFilePath}} に作成",
|
||||
"no_anonymized_database_yet": "匿名化されたデータベースはまだありません。"
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "データベースの整合性チェック",
|
||||
"description": "これは、データベースがSQLiteレベルで破損がないかをチェックします。DBのサイズによっては時間がかかる場合があります。",
|
||||
"check_button": "データベースの整合性をチェック",
|
||||
"checking_integrity": "データベースの整合性をチェックしています...",
|
||||
"integrity_check_succeeded": "整合性チェックに成功 - 問題は見つかりませんでした。",
|
||||
"integrity_check_failed": "整合性チェックに失敗: {{results}}"
|
||||
},
|
||||
"code-editor-options": {
|
||||
"title": "エディター"
|
||||
},
|
||||
"search_string": {
|
||||
"title_column": "文字列検索:",
|
||||
"search_syntax": "検索構文",
|
||||
"also_see": "詳しくは",
|
||||
"complete_help": "検索構文に関する完全なヘルプ",
|
||||
"full_text_search": "テキストを入力すると全文検索が可能",
|
||||
"label_abc": "ラベルabcを持つノートを返す",
|
||||
"label_year": "ラベル「year」の値が「2019」と一致するノート",
|
||||
"label_rock_pop": "rock と pop のラベルを持つノート",
|
||||
"label_rock_or_pop": "どれかのラベルが存在すること",
|
||||
"label_year_comparison": "数値比較(>、>=、<も含む)。",
|
||||
"label_date_created": "過去1ヶ月以内に作成されたノート",
|
||||
"error": "検索エラー: {{error}}",
|
||||
"search_prefix": "検索:"
|
||||
},
|
||||
"delete_revisions": {
|
||||
"delete_note_revisions": "ノートの変更履歴を削除",
|
||||
"all_past_note_revisions": "一致したノートの過去の変更履歴がすべて削除されます。ノート自体は完全に保持されます。言い換えると、ノートのリビジョンが削除されます。"
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "ノート名を変更",
|
||||
"new_note_title": "新しいノート名",
|
||||
"rename_note_title_to": "ノート名を変更",
|
||||
"example_note": "<code>Note</code> - マッチしたノートの名前をすべて'Note'に変更",
|
||||
"example_new_title": "<code>NEW: ${note.title}</code> - 一致したノートの名前の前に 'NEW: ' を付ける",
|
||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - マッチしたノートの前にノートの作成月日を付ける",
|
||||
"api_docs": "詳細については、 <a href='https://zadam.github.io/trilium/backend_api/Note.html'>note</a> および <a href='https://day.js.org/docs/en/display/format'>dateCreatedObj / utcDateCreatedObj properties</a> の API ドキュメントを参照してください。",
|
||||
"evaluated_as_js_string": "与えられた値はJavaScript文字列として評価されるため、注入された<code>note</code>変数(noteは名前が変更されます)を介して動的なコンテンツで強化できます。例:"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "デスクトップアプリケーション",
|
||||
"native-title-bar": "ネイティブタイトルバー",
|
||||
"native-title-bar-description": "WindowsとmacOSでは、ネイティブタイトルバーをオフにしておくと、アプリケーションがよりコンパクトに見えます。Linuxでは、ネイティブタイトルバーを表示したままの方が、他のシステムとの統一性が高まります。",
|
||||
"background-effects": "背景効果を有効化(Windows 11のみ)",
|
||||
"background-effects-description": "Mica効果は、アプリのウィンドウにぼかされたスタイリッシュな背景を追加し、奥行きとモダンな外観を作り出します。",
|
||||
"restart-app-button": "アプリケーションを再起動して変更を反映",
|
||||
"zoom-factor": "ズーム倍率"
|
||||
},
|
||||
"zoom_factor": {
|
||||
"description": "ズームは CTRL+- と CTRL+= のショートカットでも操作可能。",
|
||||
"title": "ズーム倍率(デスクトップビルドのみ)"
|
||||
"save-changes": "変更を保存して適用"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "O notatkach Trilium",
|
||||
"homepage": "Strona główna:",
|
||||
"app_version": "Wersja aplikacji:",
|
||||
"db_version": "Wersja bazy danych:",
|
||||
"sync_version": "Wersja synchronizacji:",
|
||||
"build_date": "Zbudowano:",
|
||||
"build_revision": "Rewizja zbudowania:",
|
||||
"data_directory": "Katalog z danymi:"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Błąd krytyczny",
|
||||
"message": "Wystąpił krytyczny błąd uniemożliwiający uruchomienie aplikacji:\n\n{{message}}\n\nJest to spowodowane najprawdopodobniej niespodziewanym błędem skryptu. Spróbuj uruchomić aplikację ponownie w trybie bezpiecznym i zaadresuj problem."
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Dodaj link"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Zapisz"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etykiety",
|
||||
"notes": "Notatki",
|
||||
"other": "Inne",
|
||||
"relations": "Powiązania"
|
||||
},
|
||||
"confirm": {
|
||||
"ok": "OK",
|
||||
"cancel": "Anuluj"
|
||||
},
|
||||
"delete_notes": {
|
||||
"cancel": "Anuluj",
|
||||
"close": "Zamknij"
|
||||
},
|
||||
"export": {
|
||||
"close": "Zamknij"
|
||||
}
|
||||
}
|
||||
@@ -655,8 +655,7 @@
|
||||
"showSQLConsole": "afișează consola SQL",
|
||||
"tabShortcuts": "Scurtături pentru tab-uri",
|
||||
"troubleshooting": "Unelte pentru depanare",
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou",
|
||||
"title": "Ghid rapid"
|
||||
"newTabWithActivationNoteLink": "pe o legătură către o notiță deschide și activează notița într-un tab nou"
|
||||
},
|
||||
"hide_floating_buttons_button": {
|
||||
"button_title": "Ascunde butoanele"
|
||||
@@ -887,8 +886,7 @@
|
||||
"modal_title": "Selectați tipul notiței",
|
||||
"templates": "Șabloane",
|
||||
"change_path_prompt": "Selectați locul unde să se creeze noua notiță:",
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)",
|
||||
"builtin_templates": "Șabloane predefinite"
|
||||
"search_placeholder": "căutare cale notiță după nume (cea implicită dacă este necompletat)"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Butonul „{{componentId}}” nu are nicio acțiune la clic definită"
|
||||
@@ -942,9 +940,7 @@
|
||||
},
|
||||
"password_not_set": {
|
||||
"body1": "Notițele protejate sunt criptate utilizând parola de utilizator, dar nu a fost setată nicio parolă.",
|
||||
"title": "Parola nu este setată",
|
||||
"body2": "Pentru a putea proteja notițe, clic pe butonul de mai jos pentru a deschide fereastra de opțiuni și pentru a seta parola.",
|
||||
"go_to_password_options": "Mergi la setările de parolă"
|
||||
"title": "Parola nu este setată"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"add_new_attribute": "Adaugă un nou atribut",
|
||||
@@ -1076,7 +1072,7 @@
|
||||
"note_revisions": "Revizii ale notiței"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <doc>wiki-ul</doc> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
|
||||
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
|
||||
},
|
||||
@@ -1247,10 +1243,7 @@
|
||||
"layout-horizontal-description": "bara de lansare se află sub bara de taburi, bara de taburi este pe toată lungimea.",
|
||||
"layout-horizontal-title": "Orizontal",
|
||||
"layout-vertical-title": "Vertical",
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)",
|
||||
"auto_theme": "Tema clasică (se adaptează la schema de culori a sistemului)",
|
||||
"light_theme": "Tema clasică (luminoasă)",
|
||||
"dark_theme": "Tema clasică (întunecată)"
|
||||
"layout-vertical-description": "bara de lansare se află pe stânga (implicit)"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
@@ -1871,7 +1864,7 @@
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "Format dată/timp personalizat",
|
||||
"description": "Personalizați formatul de dată și timp inserat prin <shortcut /> sau din bara de unelte. Vedeți <doc>Documentația Day.js</doc> pentru câmpurile de formatare disponibile.",
|
||||
"description": "Personalizați formatul de dată și timp inserat prin <kbd></kbd> sau din bara de unelte. Vedeți <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Documentația Day.js</a> pentru câmpurile de formatare disponibile.",
|
||||
"format_string": "Șir de formatare:",
|
||||
"formatted_time": "Data și ora formatate:"
|
||||
},
|
||||
@@ -1992,12 +1985,6 @@
|
||||
"open_externally": "Deschide în afara programului"
|
||||
},
|
||||
"modal": {
|
||||
"close": "Închide",
|
||||
"help_title": "Afișează mai multe informații despre acest ecran"
|
||||
},
|
||||
"call_to_action": {
|
||||
"background_effects_title": "Efectele de fundal sunt acum stabile",
|
||||
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
|
||||
"background_effects_button": "Activează efectele de fundal"
|
||||
"close": "Închide"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,9 +15,9 @@
|
||||
"message": "發生了嚴重錯誤,導致客戶端應用程式無法啟動:\n\n{{message}}\n\n這很可能是由於腳本以意外的方式失敗引起的。請嘗試以安全模式啟動應用程式並解決問題。"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "元件初始化失敗",
|
||||
"message-custom": "來自 ID 為 \"{{id}}\"、標題為 \"{{title}}\" 的筆記的自訂元件因以下原因無法初始化:\n\n{{message}}",
|
||||
"message-unknown": "未知元件因以下原因無法初始化:\n\n{{message}}"
|
||||
"title": "小工具初始化失敗",
|
||||
"message-custom": "來自 ID 為 \"{{id}}\"、標題為 \"{{title}}\" 的筆記的自訂小工具因以下原因無法初始化:\n\n{{message}}",
|
||||
"message-unknown": "未知小工具因以下原因無法初始化:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "載入自訂腳本失敗",
|
||||
@@ -46,10 +46,10 @@
|
||||
"affected_notes": "受影響的筆記",
|
||||
"include_descendants": "包括所選筆記的子筆記",
|
||||
"available_actions": "可用操作",
|
||||
"chosen_actions": "已選操作",
|
||||
"chosen_actions": "選擇的操作",
|
||||
"execute_bulk_actions": "執行批次操作",
|
||||
"bulk_actions_executed": "已成功執行批次操作。",
|
||||
"none_yet": "暫無操作…透過點擊上方的可用操作新增一個操作。",
|
||||
"none_yet": "暫無操作…通過點擊上方的可用操作新增一個操作。",
|
||||
"labels": "標籤",
|
||||
"relations": "關聯",
|
||||
"notes": "筆記",
|
||||
@@ -63,7 +63,7 @@
|
||||
"search_for_note_by_its_name": "依名稱搜尋筆記",
|
||||
"cloned_note_prefix_title": "克隆的筆記將在筆記樹中顯示給定的前綴",
|
||||
"prefix_optional": "前綴(可選)",
|
||||
"clone_to_selected_note": "克隆至所選的筆記",
|
||||
"clone_to_selected_note": "克隆至選定的筆記",
|
||||
"no_path_to_clone_to": "沒有克隆路徑。",
|
||||
"note_cloned": "筆記 \"{{clonedTitle}}\" 已克隆至 \"{{targetTitle}}\""
|
||||
},
|
||||
@@ -72,13 +72,13 @@
|
||||
"cancel": "取消",
|
||||
"ok": "確定",
|
||||
"are_you_sure_remove_note": "確定要從關聯圖中移除筆記 \"{{title}}\" ? ",
|
||||
"if_you_dont_check": "如果不勾選此項,筆記將僅從關聯圖中移除。",
|
||||
"if_you_dont_check": "如果不選中此項,筆記將僅從關聯圖中移除。",
|
||||
"also_delete_note": "同時刪除筆記"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "刪除筆記預覽",
|
||||
"delete_all_clones_description": "同時刪除所有克隆(可以在最近修改中撤消)",
|
||||
"erase_notes_description": "通常(軟)刪除僅標記筆記為已刪除,可以在一段時間內透過最近修改對話方塊撤消。勾選此選項將立即擦除筆記,無法撤銷。",
|
||||
"erase_notes_description": "通常(軟)刪除僅標記筆記為已刪除,可以在一段時間內通過最近修改對話方塊撤消。選中此選項將立即擦除筆記,無法撤銷。",
|
||||
"erase_notes_warning": "永久擦除筆記(無法撤銷),包括所有克隆。這將強制應用程式重新載入。",
|
||||
"notes_to_be_deleted": "將刪除以下筆記 ({{notesCount}})",
|
||||
"no_note_to_delete": "沒有筆記將被刪除(僅克隆)。",
|
||||
@@ -92,18 +92,18 @@
|
||||
"export_note_title": "匯出筆記",
|
||||
"close": "關閉",
|
||||
"export_type_subtree": "此筆記及其所有子筆記",
|
||||
"format_html_zip": "HTML ZIP 封存 - 建議使用此選項,因為它保留了所有格式。",
|
||||
"format_html_zip": "HTML ZIP 歸檔 - 建議使用此選項,因為它保留了所有格式。",
|
||||
"format_markdown": "Markdown - 保留大部分格式。",
|
||||
"format_opml": "OPML - 大綱交換格式,僅限文字。不包括格式、圖片和檔案。",
|
||||
"format_opml": "OPML - 大綱交換格式,僅限文字。不包括格式、圖片和文件。",
|
||||
"opml_version_1": "OPML v1.0 - 僅限純文字",
|
||||
"opml_version_2": "OPML v2.0 - 還允許 HTML",
|
||||
"export_type_single": "僅此筆記,不包括子筆記",
|
||||
"export": "匯出",
|
||||
"choose_export_type": "請先選擇匯出類型",
|
||||
"export_status": "匯出狀態",
|
||||
"export_in_progress": "正在匯出:{{progressCount}}",
|
||||
"export_in_progress": "匯出進行中:{{progressCount}}",
|
||||
"export_finished_successfully": "成功匯出。",
|
||||
"format_html": "HTML - 推薦,因為它保留了所有格式",
|
||||
"format_html": "HTML - 推薦,因為它保留所有格式",
|
||||
"format_pdf": "PDF - 用於列印或與他人分享。"
|
||||
},
|
||||
"help": {
|
||||
@@ -113,37 +113,37 @@
|
||||
"notSet": "未設定",
|
||||
"goBackForwards": "在歷史記錄中前後移動",
|
||||
"showJumpToNoteDialog": "顯示<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"跳轉至\" 對話方塊</a>",
|
||||
"scrollToActiveNote": "捲動至使用中筆記",
|
||||
"scrollToActiveNote": "滾動至活動筆記",
|
||||
"jumpToParentNote": "跳轉至父級筆記",
|
||||
"collapseWholeTree": "收摺整個筆記樹",
|
||||
"collapseSubTree": "收摺子階層",
|
||||
"tabShortcuts": "標籤快捷鍵",
|
||||
"onlyInDesktop": "僅在桌面版(以 Electron 構建)中",
|
||||
"openEmptyTab": "打開空白分頁",
|
||||
"closeActiveTab": "關閉使用中分頁",
|
||||
"closeActiveTab": "關閉活動分頁",
|
||||
"activateNextTab": "切換至下一個分頁",
|
||||
"activatePreviousTab": "切換至上一個分頁",
|
||||
"creatingNotes": "新增筆記",
|
||||
"createNoteAfter": "在使用中筆記後新增筆記",
|
||||
"createNoteInto": "在使用中筆記中新增新子筆記",
|
||||
"editBranchPrefix": "編輯使用中筆記克隆的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">前綴</a>",
|
||||
"createNoteAfter": "在活動筆記後新增筆記",
|
||||
"createNoteInto": "在活動筆記中新增新子筆記",
|
||||
"editBranchPrefix": "編輯活動筆記克隆的<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">前綴</a>",
|
||||
"movingCloningNotes": "移動/克隆筆記",
|
||||
"moveNoteUpDown": "在筆記列表中向上/向下移動筆記",
|
||||
"moveNoteUpHierarchy": "在層級結構中向上移動筆記",
|
||||
"multiSelectNote": "多選上/下筆記",
|
||||
"selectAllNotes": "選擇目前級別的所有筆記",
|
||||
"selectNote": "選擇筆記",
|
||||
"copyNotes": "將使用中筆記(或目前選擇)複製到剪貼簿(用於<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/cloning-notes.html#cloning-notes\">克隆</a>)",
|
||||
"cutNotes": "將目前筆記(或目前選擇)剪下到剪貼簿(用於移動筆記)",
|
||||
"pasteNotes": "將筆記貼上為使用中筆記的子筆記(根據是複製還是剪下至剪貼簿來決定是移動還是克隆)",
|
||||
"copyNotes": "將活動筆記(或目前選擇)複製到剪貼簿(用於<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/cloning-notes.html#cloning-notes\">克隆</a>)",
|
||||
"cutNotes": "將當前筆記(或當前選擇)剪下到剪貼簿(用於移動筆記)",
|
||||
"pasteNotes": "將筆記貼上為活動筆記的子筆記(根據是複製還是剪下至剪貼簿來決定是移動還是克隆)",
|
||||
"deleteNotes": "刪除筆記/子階層",
|
||||
"editingNotes": "編輯筆記",
|
||||
"editNoteTitle": "在筆記樹中,焦點會從筆記樹切換至筆記標題。按下 Enter 鍵會將焦點從筆記標題切換至文字編輯器。按下 <kbd>Ctrl+.</kbd> 會將焦點從編輯器切換回筆記樹。",
|
||||
"editNoteTitle": "在樹形筆記樹中,焦點會從筆記樹切換至筆記標題。按下 Enter 鍵會將焦點從筆記標題切換至文字編輯器。按下 <kbd>Ctrl+.</kbd> 會將焦點從編輯器切換回筆記樹。",
|
||||
"createEditLink": "新增/編輯外部連結",
|
||||
"createInternalLink": "新增內部連結",
|
||||
"followLink": "跟隨游標下的連結",
|
||||
"insertDateTime": "在插入點插入目前日期和時間",
|
||||
"jumpToTreePane": "跳轉至筆記樹面板並捲動至使用中筆記",
|
||||
"jumpToTreePane": "跳轉至樹面板並滾動至活動筆記",
|
||||
"markdownAutoformat": "類 Markdown自動格式化",
|
||||
"headings": "<code>##</code>, <code>###</code>, <code>####</code> 等,後跟空格,自動轉換為標題",
|
||||
"bulletList": "<code>*</code> 或 <code>-</code> 後跟空格,自動轉換為項目符號列表",
|
||||
@@ -156,23 +156,23 @@
|
||||
"other": "其他",
|
||||
"quickSearch": "定位至快速搜尋框",
|
||||
"inPageSearch": "頁面內搜尋",
|
||||
"title": "列表",
|
||||
"title": "資料表",
|
||||
"newTabNoteLink": "在新分頁開啟筆記連結",
|
||||
"newTabWithActivationNoteLink": "在新分頁開啟並切換至筆記連結"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "匯入至筆記",
|
||||
"chooseImportFile": "選擇檔案匯入",
|
||||
"importDescription": "所選檔案的內容將作為子筆記匯入至",
|
||||
"chooseImportFile": "選擇匯入文件",
|
||||
"importDescription": "所選文件的內容將作為子筆記匯入至",
|
||||
"options": "選項",
|
||||
"safeImportTooltip": "Trilium <code>.zip</code> 匯出的檔案可能包含可能有害的可執行腳本。安全匯入將停用所有匯入腳本的自動執行。僅當您完全信任匯入的可執行腳本的內容時,才取消勾選「安全匯入」。",
|
||||
"safeImportTooltip": "Trilium <code>.zip</code> 匯出文件可能包含可能有害的可執行腳本。安全匯入將停用所有匯入腳本的自動執行。僅當您完全信任匯入的可執行腳本的內容時,才取消選中「安全匯入」。",
|
||||
"safeImport": "安全匯入",
|
||||
"explodeArchivesTooltip": "如果勾選此項,則 Trilium 將讀取<code>.zip</code>、<code>.enex</code>和<code>.opml</code>文件,並從這些封存文件內部的檔案新增筆記。如果未勾選,則 Trilium 會將這些封存文件本身附加至筆記中。",
|
||||
"explodeArchives": "讀取<code>.zip</code>、<code>.enex</code>和<code>.opml</code>封存文件的內容。",
|
||||
"shrinkImagesTooltip": "<p>如果勾選此選項,Trilium 將嘗試透過縮放和最佳化來縮小匯入的圖片,這可能會影響圖片的觀看品質。如果未勾選,圖片將不做修改直接匯入。</p><p>這不適用於帶有詮釋資料的<code>.zip</code>匯入,因為這些文件被認為已最佳化。</p>",
|
||||
"explodeArchivesTooltip": "如果選中此項,則Trilium將讀取<code>.zip</code>、<code>.enex</code>和<code>.opml</code>文件,並從這些歸檔文件內部的文件新增筆記。如果未選中,則Trilium會將這些歸檔文件本身附加至筆記中。",
|
||||
"explodeArchives": "讀取<code>.zip</code>、<code>.enex</code>和<code>.opml</code>歸檔文件的內容。",
|
||||
"shrinkImagesTooltip": "<p>如果選中此選項,Trilium將嘗試通過縮放和優化來縮小匯入的圖片,這可能會影響圖片的感知質量。如果未選中,圖片將不做修改地匯入。</p><p>這不適用於帶有元數據的<code>.zip</code>匯入,因為這些文件已被假定為已優化。</p>",
|
||||
"shrinkImages": "壓縮圖片",
|
||||
"textImportedAsText": "如果元數據不明確,將HTML、Markdown和TXT匯入為文字筆記",
|
||||
"codeImportedAsCode": "如果詮釋資料不明確,將識別的程式碼檔案(例如<code>.json</code>)匯入為程式碼筆記",
|
||||
"codeImportedAsCode": "如果元數據不明確,將識別的程式碼文件(例如<code>.json</code>)匯入為程式碼筆記",
|
||||
"replaceUnderscoresWithSpaces": "在匯入的筆記名稱中將下劃線替換為空格",
|
||||
"import": "匯入",
|
||||
"failed": "匯入失敗: {{message}}.",
|
||||
@@ -187,14 +187,14 @@
|
||||
"successful": "匯入成功。"
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "內嵌筆記",
|
||||
"dialog_title": "包含筆記",
|
||||
"label_note": "筆記",
|
||||
"placeholder_search": "依名稱搜尋筆記",
|
||||
"box_size_prompt": "內嵌筆記的框大小:",
|
||||
"box_size_small": "小型(顯示大約 10 行)",
|
||||
"box_size_prompt": "包含筆記的框大小:",
|
||||
"box_size_small": "小型 (顯示大約10行)",
|
||||
"box_size_medium": "中型 (顯示大約30行)",
|
||||
"box_size_full": "完整顯示(完整文字框)",
|
||||
"button_include": "內嵌筆記"
|
||||
"button_include": "包含筆記"
|
||||
},
|
||||
"info": {
|
||||
"modalTitle": "資訊消息",
|
||||
@@ -209,14 +209,14 @@
|
||||
"dialog_title": "Markdown 匯入",
|
||||
"modal_body_text": "由於瀏覽器沙盒的限制,無法直接從 JavaScript 讀取剪貼簿內容。請將要匯入的 Markdown 文字貼上至下面的文字框中,然後點擊匯入按鈕",
|
||||
"import_button": "匯入",
|
||||
"import_success": "已成功匯入 Markdown 文件內容。"
|
||||
"import_success": "已成功匯入 Markdown 內容文檔。"
|
||||
},
|
||||
"move_to": {
|
||||
"dialog_title": "移動筆記至…",
|
||||
"notes_to_move": "需要移動的筆記",
|
||||
"target_parent_note": "目標父級筆記",
|
||||
"search_placeholder": "透過名稱搜尋筆記",
|
||||
"move_button": "移動至所選的筆記",
|
||||
"search_placeholder": "通過名稱搜尋筆記",
|
||||
"move_button": "移動至選定的筆記",
|
||||
"error_no_path": "沒有可以移動至的路徑。",
|
||||
"move_success_message": "已移動所選筆記至 "
|
||||
},
|
||||
@@ -268,7 +268,7 @@
|
||||
"revision_restored": "已還原筆記歷史版本。",
|
||||
"revision_deleted": "已刪除筆記歷史版本。",
|
||||
"snapshot_interval": "筆記快照儲存間隔: {{seconds}}秒。",
|
||||
"maximum_revisions": "目前筆記的最大歷史數量: {{number}}。",
|
||||
"maximum_revisions": "當前筆記的最歷史數量: {{number}}。",
|
||||
"settings": "筆記歷史設定",
|
||||
"download_button": "下載",
|
||||
"mime": "MIME類型: ",
|
||||
@@ -297,18 +297,18 @@
|
||||
},
|
||||
"upload_attachments": {
|
||||
"upload_attachments_to_note": "上傳附件至筆記",
|
||||
"choose_files": "選擇檔案",
|
||||
"files_will_be_uploaded": "檔案將作為附件上傳至 {{noteTitle}}",
|
||||
"choose_files": "選擇文件",
|
||||
"files_will_be_uploaded": "文件將作為附件上傳至 {{noteTitle}}",
|
||||
"options": "選項",
|
||||
"shrink_images": "縮小圖片",
|
||||
"upload": "上傳",
|
||||
"tooltip": "如果您勾選此選項,Trilium 將嘗試透過縮放和最佳化來縮小上傳的圖片,這可能會影響圖片的觀看品質。如果未勾選,則將不進行修改直接上傳圖片。"
|
||||
"tooltip": "如果您勾選此選項,Trilium 將嘗試通過縮放和優化來縮小上傳的圖片,這可能會影響感知的圖片質量。如果未選中,則將以不進行修改的方式上傳圖片。"
|
||||
},
|
||||
"attribute_detail": {
|
||||
"attr_detail_title": "屬性內容標題",
|
||||
"attr_detail_title": "屬性詳情標題",
|
||||
"close_button_title": "取消修改並關閉",
|
||||
"attr_is_owned_by": "屬性所有者",
|
||||
"attr_name_title": "屬性名稱只能由字母、數字、冒號和下劃線組成",
|
||||
"attr_name_title": "屬性名稱只能由字母數字字符、冒號和下劃線組成",
|
||||
"name": "名稱",
|
||||
"value": "值",
|
||||
"target_note_title": "關聯是來源筆記和目標筆記之間的命名連接。",
|
||||
@@ -341,33 +341,33 @@
|
||||
"delete": "刪除",
|
||||
"related_notes_title": "含有此標籤的其他筆記",
|
||||
"more_notes": "更多筆記",
|
||||
"label": "標籤內容",
|
||||
"label_definition": "標籤定義內容",
|
||||
"relation": "關聯內容",
|
||||
"relation_definition": "關聯定義內容",
|
||||
"label": "標籤詳情",
|
||||
"label_definition": "標籤定義詳情",
|
||||
"relation": "關聯詳情",
|
||||
"relation_definition": "關聯定義詳情",
|
||||
"disable_versioning": "禁用自動版本控制。適用於例如大型但不重要的筆記 - 例如用於腳本編寫的大型JS庫",
|
||||
"calendar_root": "標記應用作為每日筆記的根。只應標記一個筆記。",
|
||||
"archived": "含有此標籤的筆記預設在搜尋結果中不可見(也適用於跳轉至、新增連結對話方塊等)。",
|
||||
"exclude_from_export": "筆記(及其子階層)不會包含在任何匯出的筆記中",
|
||||
"exclude_from_export": "筆記(及其子階層)不會包含在任何筆記匯出中",
|
||||
"run": "定義腳本應運行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端啟動時(或重新整理時),但不會在移動端執行。</li>\n<li>mobileStartup - Trilium前端啟動時(或重新整理時), 在行動端會執行。</li>\n<li>backendStartup - Trilium後端啟動時</li>\n<li>hourly - 每小時運行一次。您可以使用附加標籤<code>runAtHour</code>指定小時。</li>\n<li>daily - 每天運行一次</li>\n</ul>",
|
||||
"run_on_instance": "定義應在哪個 Trilium 實例上運行。預設為所有實例。",
|
||||
"run_at_hour": "應在哪個小時運行。應與<code>#run=hourly</code>一起使用。可以多次定義,以便一天內運行多次。",
|
||||
"disable_inclusion": "含有此標籤的腳本不會包含在父腳本執行中。",
|
||||
"sorted": "依標題字母順序保持子筆記排序",
|
||||
"sorted": "按標題字母順序保持子筆記排序",
|
||||
"sort_direction": "ASC(預設)或DESC",
|
||||
"sort_folders_first": "資料夾(含有子筆記的筆記)應排在頂部",
|
||||
"top": "在其父級中保留給定筆記在頂部(僅適用於排序的父級)",
|
||||
"hide_promoted_attributes": "隱藏此筆記上的升級屬性",
|
||||
"read_only": "編輯器處於唯讀模式。僅適用於文字和程式碼筆記。",
|
||||
"auto_read_only_disabled": "文字 / 程式碼筆記可以在太大時自動設定為唯讀模式。您可以透過向筆記新增此標籤來對單個筆記單獨設定禁用唯讀。",
|
||||
"auto_read_only_disabled": "文字/程式碼筆記可以在太大時自動設定為唯讀模式。您可以通過向筆記新增此標籤來對單個筆記單獨設定禁用唯讀。",
|
||||
"app_css": "標記載入至 Trilium 應用程式中的 CSS 筆記,因此可以用於修改 Trilium 的外觀。",
|
||||
"app_theme": "標記為完整的Trilium主題的CSS筆記,因此可以在Trilium選項中使用。",
|
||||
"css_class": "該標籤的值將作為 CSS 類新增至樹中表示給定筆記的節點。這對於進階主題設定非常有用。可用於模板筆記。",
|
||||
"icon_class": "該標籤的值將作為 CSS 類新增至樹中圖標上,有助於從視覺上區分筆記樹裡的筆記。比如可以是 bx bx-home——圖標來自 boxicons。可用於模板筆記。",
|
||||
"css_class": "該標籤的值將作為 CSS 類新增至樹中表示給定筆記的節點。這對於高級主題設定非常有用。可用於模板筆記。",
|
||||
"icon_class": "該標籤的值將作為 CSS 類新增至樹中圖標上,有助於從視覺上區分筆記樹里的筆記。比如可以是 bx bx-home——圖標來自 boxicons。可用於模板筆記。",
|
||||
"page_size": "筆記列表中每頁的項目數",
|
||||
"custom_request_handler": "請參閱<a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">自訂請求處理程序</a>",
|
||||
"custom_resource_provider": "請參閱<a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">自訂請求處理程序</a>",
|
||||
"widget": "將此筆記標記為將新增至 Trilium 元件樹中的自訂元件",
|
||||
"widget": "將此筆記標記為將新增至 Trilium 組件樹中的自訂小工具",
|
||||
"workspace": "將此筆記標記為允許輕鬆聚焦的工作區",
|
||||
"workspace_icon_class": "定義在選項卡中聚焦至此筆記時將使用的框圖圖標 CSS 類",
|
||||
"workspace_tab_background_color": "聚焦至此筆記時在筆記選項卡中使用的CSS顏色",
|
||||
@@ -375,33 +375,33 @@
|
||||
"workspace_template": "在新增新筆記時,此筆記將出現在可用模板的選擇中,但僅當聚焦至包含此模板的工作區時",
|
||||
"search_home": "新的搜尋筆記將作為此筆記的子筆記新增",
|
||||
"workspace_search_home": "當聚焦至此工作區筆記的某個祖先時,新的搜尋筆記將作為此筆記的子筆記新增",
|
||||
"inbox": "使用側邊欄中的「新建筆記」按鈕新增筆記時,預設收件匣位置。筆記將作為標有 <code>#inbox</code> 標籤的筆記的子筆記新增。",
|
||||
"inbox": "使用側邊欄中的\"新建筆記\"按鈕新增筆記時,預設收件匣位置。筆記將作為標有<code>#inbox</code>標籤的筆記的子筆記新增。",
|
||||
"workspace_inbox": "當聚焦至此工作區筆記的某個父級筆記時,新筆記的預設收件匣位置",
|
||||
"sql_console_home": "SQL控制台筆記的預設位置",
|
||||
"bookmark_folder": "含有此標籤的筆記將作為資料夾出現在書籤中(允許訪問其子筆記)",
|
||||
"share_hidden_from_tree": "此筆記從左側導航樹中隱藏,但仍可透過其URL訪問",
|
||||
"share_hidden_from_tree": "此筆記從左側導航樹中隱藏,但仍可通過其URL訪問",
|
||||
"share_external_link": "筆記將在分享樹中作為指向外部網站的連結",
|
||||
"share_alias": "使用此別名定義將在 https://您的trilium域名/share/[別名] 下可用的筆記",
|
||||
"share_omit_default_css": "將省略預設的分享頁面CSS。當您進行廣泛的樣式修改時使用。",
|
||||
"share_root": "標記作為在 /share 地址分享的根節點筆記。",
|
||||
"share_description": "定義要新增至 HTML meta 標籤以供描述的文字",
|
||||
"share_raw": "筆記將以其原始格式提供,不帶HTML包裝器",
|
||||
"share_disallow_robot_indexing": "將透過<code>X-Robots-Tag: noindex</code>標頭禁止爬蟲機器人索引此筆記",
|
||||
"share_credentials": "需要憑證才能訪問此分享筆記。值應以'username:password'格式提供。請勿忘記使其可繼承以套用於子筆記 / 圖片。",
|
||||
"share_disallow_robot_indexing": "將通過<code>X-Robots-Tag: noindex</code>標頭禁止爬蟲機器人索引此筆記",
|
||||
"share_credentials": "需要憑據才能訪問此分享筆記。值應以'username:password'格式提供。請勿忘記使其可繼承以應用於子筆記/圖片。",
|
||||
"share_index": "含有此標籤的筆記將列出所有分享筆記的根",
|
||||
"display_relations": "應顯示的逗號分隔關聯名稱。將隱藏所有其他關聯。",
|
||||
"hide_relations": "應隱藏的逗號分隔關聯名稱。將顯示所有其他關聯。",
|
||||
"title_template": "新增為此筆記的子筆記時的預設標題。該值將作為 JavaScript 字串評估\n 並因此可以透過注入的 <code>now</code> 和 <code>parentNote</code> 變數豐富動態內容。範例:\n \n <ul>\n <li><code>${parentNote.getLabelValue('authorName')}的文學作品</code></li>\n <li><code>Log for ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n 有關詳細資訊,請參見<a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">詳細資訊 wiki</a>,API 文件<a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a>和<a href=\"https://day.js.org/docs/en/display/format\">now</a>。",
|
||||
"title_template": "新增為此筆記的子筆記時的預設標題。該值將作為JavaScript字符串評估\n 並因此可以通過注入的<code>now</code>和<code>parentNote</code>變量豐富動態內容。示例:\n \n <ul>\n <li><code>${parentNote.getLabelValue('authorName')}的文學作品</code></li>\n <li><code>Log for ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n 有關詳細資訊,請參見<a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">詳細資訊wiki</a>,API文檔<a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a>和<a href=\"https://day.js.org/docs/en/display/format\">now</a>。",
|
||||
"template": "新增新筆記時將出現在可用模板的選擇中的筆記",
|
||||
"toc": "<code>#toc</code>或<code>#toc=show</code>將強制顯示目錄。 <code>#toc=hide</code>將強制隱藏它。如果標籤不存在,則依照全域設定",
|
||||
"toc": "<code>#toc</code>或<code>#toc=show</code>將強制顯示目錄。 <code>#toc=hide</code>將強制隱藏它。如果標籤不存在,則觀察全局設定",
|
||||
"color": "定義筆記樹、連結等中筆記的顏色。使用任何有效的CSS顏色值,如'red'或#a13d5f",
|
||||
"keyboard_shortcut": "定義立即跳轉至此筆記的鍵盤快捷鍵。示例:'ctrl+alt+e'。需要前端重新載入才能生效。",
|
||||
"keep_current_hoisting": "即使筆記不在目前聚焦的子階層中顯示,打開此連結也不會修改聚焦。",
|
||||
"execute_button": "將執行目前程式碼筆記按鈕的標題",
|
||||
"execute_description": "目前程式碼筆記更長的描述與執行按鈕一同顯示",
|
||||
"keep_current_hoisting": "即使筆記不在當前聚焦的子階層中顯示,打開此連結也不會修改聚焦。",
|
||||
"execute_button": "將執行當前程式碼筆記的按鈕標題",
|
||||
"execute_description": "顯示與執行按鈕一起顯示的當前程式碼筆記的更長描述",
|
||||
"exclude_from_note_map": "含有此標籤的筆記將從筆記地圖中隱藏",
|
||||
"new_notes_on_top": "新筆記將新增在父級筆記的頂部,而不是底部。",
|
||||
"hide_highlight_widget": "隱藏高亮列表元件",
|
||||
"hide_highlight_widget": "隱藏高亮列表小工具",
|
||||
"run_on_note_creation": "在後端新增筆記時執行。如果要為在特定子階層下新增的所有筆記運行腳本,請使用此關聯。在這種情況下,在子階層根筆記上新增它並使其可繼承。在子階層中的任何深度新增新筆記都會觸發腳本。",
|
||||
"run_on_child_note_creation": "當新增新的子筆記時執行",
|
||||
"run_on_note_title_change": "當筆記標題修改時執行(包括筆記新增)",
|
||||
@@ -416,14 +416,14 @@
|
||||
"relation_template": "即使沒有上下級關係,筆記的屬性也將繼承。如果為空,則筆記的內容和子階層將新增至實例筆記中。有關詳細資訊,請參見文檔。",
|
||||
"inherit": "即使沒有上下級關係,筆記的屬性也將繼承。有關類似概念的模板關係,請參見模板關係。請參閱文檔中的屬性繼承。",
|
||||
"render_note": "「渲染HTML筆記」類型的筆記將使用程式碼筆記(HTML或腳本)進行呈現,因此需要指定要渲染的筆記",
|
||||
"widget_relation": "此關聯的目標將作為側邊欄中的元件執行和呈現",
|
||||
"widget_relation": "此關聯的目標將作為側邊欄中的小工具執行和呈現",
|
||||
"share_css": "將注入分享頁面的CSS筆記。CSS筆記也必須位於分享子階層中。可以考慮一並使用'share_hidden_from_tree'和'share_omit_default_css'。",
|
||||
"share_js": "將注入分享頁面的JavaScript筆記。JS筆記也必須位於分享子階層中。可以考慮一並使用'share_hidden_from_tree'。",
|
||||
"share_template": "用作顯示分享筆記的模板的嵌入式JavaScript筆記。如果沒有,將回退至預設模板。可以考慮一並使用'share_hidden_from_tree'。",
|
||||
"share_favicon": "在分享頁面中設定的favicon筆記。一般需要將它設定為分享和可繼承。Favicon筆記也必須位於分享子階層中。可以考慮一並使用'share_hidden_from_tree'。",
|
||||
"is_owned_by_note": "由此筆記所有",
|
||||
"other_notes_with_name": "其他含有 {{attributeType}} 名為 \"{{attributeName}}\" 的的筆記",
|
||||
"and_more": "…以及另外 {{count}} 個。",
|
||||
"and_more": "…以及另外 {{count}} 個",
|
||||
"app_theme_base": "設定為 \"next\"、\"next-light \" 或 \"next-dark\",以使用相應的 TriliumNext 主題(自動、淺色或深色)作為自訂主題的基礎,而非傳統主題。",
|
||||
"print_landscape": "匯出為 PDF 時,將頁面方向更改為橫向而非縱向。",
|
||||
"print_page_size": "在匯出 PDF 時更改頁面大小。支援的值:<code>A0</code>、<code>A1</code>、<code>A2</code>、<code>A3</code>、<code>A4</code>、<code>A5</code>、<code>A6</code>、<code>Legal</code>、<code>Letter</code>、<code>Tabloid</code>、<code>Ledger</code>。",
|
||||
@@ -447,7 +447,7 @@
|
||||
"execute_script": {
|
||||
"execute_script": "執行腳本",
|
||||
"help_text": "您可以在匹配的筆記上執行簡單的腳本。",
|
||||
"example_1": "例如,要在筆記標題後附加字串,請使用以下腳本:",
|
||||
"example_1": "例如,要在筆記標題後附加字符串,請使用以下腳本:",
|
||||
"example_2": "更複雜的例子,刪除所有匹配的筆記屬性:"
|
||||
},
|
||||
"add_label": {
|
||||
@@ -488,7 +488,7 @@
|
||||
"delete_matched_notes": "刪除匹配的筆記",
|
||||
"delete_matched_notes_description": "這將刪除匹配的筆記。",
|
||||
"undelete_notes_instruction": "刪除後,可以從「最近修改」對話方塊中還原它們。",
|
||||
"erase_notes_instruction": "要永久擦除筆記,您可以在刪除後至「選項」→「其他」,然後點擊「立即擦除已刪除的筆記」按鈕。"
|
||||
"erase_notes_instruction": "要永久擦除筆記,您可以在刪除後至「選項」->「其他」,然後點擊「立即擦除已刪除的筆記」按鈕。"
|
||||
},
|
||||
"delete_revisions": {
|
||||
"delete_note_revisions": "刪除筆記歷史",
|
||||
@@ -508,7 +508,7 @@
|
||||
"rename_note_title_to": "重新命名筆記標題為",
|
||||
"new_note_title": "新筆記標題",
|
||||
"click_help_icon": "點擊右側的說明圖示查看所有選項",
|
||||
"evaluated_as_js_string": "給定的值被認為是 JavaScript 字串,因此可以透過注入的 <code>note</code> 變數(正在重新命名的筆記)豐富動態內容。 例如:",
|
||||
"evaluated_as_js_string": "給定的值被評估為 JavaScript 字符串,因此可以通過注入的 <code>note</code> 變量(正在重新命名的筆記)豐富動態內容。 例如:",
|
||||
"example_note": "<code>Note</code> - 所有匹配的筆記都被重新命名為「Note」",
|
||||
"example_new_title": "<code>NEW: ${note.title}</code> - 匹配的筆記標題以「NEW: 」為前綴",
|
||||
"example_date_prefix": "<code>${note.dateCreatedObj.format('MM-DD:')}: ${note.title}</code> - 匹配的筆記以筆記的新增月份-日期為前綴",
|
||||
@@ -546,10 +546,10 @@
|
||||
"update_relation_target": "更新關聯目標"
|
||||
},
|
||||
"attachments_actions": {
|
||||
"open_externally": "以外部程式打開",
|
||||
"open_externally_title": "檔案將會在外部應用程式中打開並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"open_externally": "用外部程序打開",
|
||||
"open_externally_title": "文件將會在外部應用程式中打開,並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"open_custom": "自訂打開方式",
|
||||
"open_custom_title": "檔案將會在外部應用程式中打開並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"open_custom_title": "文件將會在外部應用程式中打開,並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"download": "下載",
|
||||
"rename_attachment": "重新命名附件",
|
||||
"upload_new_revision": "上傳新版本",
|
||||
@@ -559,7 +559,7 @@
|
||||
"upload_success": "已上傳新附件版本。",
|
||||
"upload_failed": "新附件版本上傳失敗。",
|
||||
"open_externally_detail_page": "外部打開附件僅在詳細頁面中可用,請首先點擊附件詳細資訊,然後重復此操作。",
|
||||
"open_custom_client_only": "自訂打開附件只能透過桌面版完成。",
|
||||
"open_custom_client_only": "自訂打開附件只能通過客戶端完成。",
|
||||
"delete_confirm": "您確定要刪除附件 '{{title}}' 嗎?",
|
||||
"delete_success": "附件 '{{title}}' 已被刪除。",
|
||||
"convert_confirm": "您確定要將附件 '{{title}}' 轉換為單獨的筆記嗎?",
|
||||
@@ -593,7 +593,7 @@
|
||||
"close_this_pane": "關閉此面板"
|
||||
},
|
||||
"create_pane_button": {
|
||||
"create_new_split": "分割頁面"
|
||||
"create_new_split": "拆分面板"
|
||||
},
|
||||
"edit_button": {
|
||||
"edit_this_note": "編輯此筆記"
|
||||
@@ -607,29 +607,29 @@
|
||||
"global_menu": {
|
||||
"menu": "選單",
|
||||
"options": "選項",
|
||||
"open_new_window": "打開新視窗",
|
||||
"open_new_window": "打開新窗口",
|
||||
"switch_to_mobile_version": "切換至行動版",
|
||||
"switch_to_desktop_version": "切換至桌面版",
|
||||
"zoom": "縮放",
|
||||
"toggle_fullscreen": "切換全螢幕",
|
||||
"toggle_fullscreen": "切換全熒幕",
|
||||
"zoom_out": "縮小",
|
||||
"reset_zoom_level": "重置縮放級別",
|
||||
"zoom_in": "放大",
|
||||
"configure_launchbar": "設定啟動欄",
|
||||
"configure_launchbar": "設定啟動列",
|
||||
"show_shared_notes_subtree": "顯示分享筆記子階層",
|
||||
"advanced": "進階",
|
||||
"open_dev_tools": "打開開發者工具",
|
||||
"advanced": "高級",
|
||||
"open_dev_tools": "打開開發工具",
|
||||
"open_sql_console": "打開SQL控制台",
|
||||
"open_sql_console_history": "打開SQL控制台歷史記錄",
|
||||
"open_search_history": "打開搜尋歷史",
|
||||
"show_backend_log": "顯示後台日誌",
|
||||
"reload_hint": "重新載入可以幫助解決一些視覺故障,而無需重新啟動整個應用程式。",
|
||||
"reload_frontend": "重新載入前端",
|
||||
"show_hidden_subtree": "顯示隱藏的子階層",
|
||||
"show_hidden_subtree": "顯示隱藏子階層",
|
||||
"show_help": "顯示說明",
|
||||
"about": "關於 TriliumNext 筆記",
|
||||
"logout": "登出",
|
||||
"show-cheatsheet": "顯示快捷鍵說明",
|
||||
"show-cheatsheet": "顯示工具表",
|
||||
"toggle-zen-mode": "禪模式"
|
||||
},
|
||||
"sync_status": {
|
||||
@@ -652,15 +652,15 @@
|
||||
"convert_into_attachment": "轉換為附件",
|
||||
"re_render_note": "重新渲染筆記",
|
||||
"search_in_note": "在筆記中搜尋",
|
||||
"note_source": "筆記原始碼",
|
||||
"note_source": "筆記源程式碼",
|
||||
"note_attachments": "筆記附件",
|
||||
"open_note_externally": "用外部程式打開筆記",
|
||||
"open_note_externally_title": "檔案將在外部應用程式中打開並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"open_note_custom": "使用自訂程式打開筆記",
|
||||
"import_files": "匯入檔案",
|
||||
"open_note_externally": "用外部程序打開筆記",
|
||||
"open_note_externally_title": "文件將在外部應用程式中打開,並監視其更改。然後您可以將修改後的版本上傳回 Trilium。",
|
||||
"open_note_custom": "使用自訂程序打開筆記",
|
||||
"import_files": "匯入文件",
|
||||
"export_note": "匯出筆記",
|
||||
"delete_note": "刪除筆記",
|
||||
"print_note": "列印筆記",
|
||||
"print_note": "打印筆記",
|
||||
"save_revision": "儲存筆記歷史",
|
||||
"convert_into_attachment_failed": "筆記 '{{title}}' 轉換失敗。",
|
||||
"convert_into_attachment_successful": "筆記 '{{title}}' 已成功轉換為附件。",
|
||||
@@ -668,14 +668,14 @@
|
||||
"print_pdf": "匯出為 PDF…"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
|
||||
"no_click_handler": "按鈕組件'{{componentId}}'沒有定義點擊處理程序"
|
||||
},
|
||||
"protected_session_status": {
|
||||
"active": "已進入受保護的作業階段。點擊退出受保護的作業階段。",
|
||||
"inactive": "點擊進入受保護的作業階段"
|
||||
"active": "受保護的會話已激活。點擊退出受保護的會話。",
|
||||
"inactive": "點擊進入受保護的會話"
|
||||
},
|
||||
"revisions_button": {
|
||||
"note_revisions": "筆記歷史版本"
|
||||
"note_revisions": "筆記修改歷史"
|
||||
},
|
||||
"update_available": {
|
||||
"update_available": "有更新可用"
|
||||
@@ -685,7 +685,7 @@
|
||||
},
|
||||
"code_buttons": {
|
||||
"execute_button_title": "執行腳本",
|
||||
"trilium_api_docs_button_title": "打開 Trilium API 文件",
|
||||
"trilium_api_docs_button_title": "打開 Trilium API 文檔",
|
||||
"save_to_note_button_title": "儲存至筆記",
|
||||
"opening_api_docs_message": "正在打開 API 文檔…",
|
||||
"sql_console_saved_message": "SQL 控制台已儲存至 {{note_path}}"
|
||||
@@ -753,20 +753,20 @@
|
||||
},
|
||||
"file_properties": {
|
||||
"note_id": "筆記 ID",
|
||||
"original_file_name": "原始檔案名稱",
|
||||
"file_type": "檔案類型",
|
||||
"file_size": "檔案大小",
|
||||
"original_file_name": "原始文件名",
|
||||
"file_type": "文件類型",
|
||||
"file_size": "文件大小",
|
||||
"download": "下載",
|
||||
"open": "打開",
|
||||
"upload_new_revision": "上傳新版本",
|
||||
"upload_success": "已上傳新檔案版本。",
|
||||
"upload_failed": "新檔案版本上傳失敗。",
|
||||
"title": "檔案"
|
||||
"upload_success": "已上傳新文件版本。",
|
||||
"upload_failed": "新文件版本上傳失敗。",
|
||||
"title": "文件"
|
||||
},
|
||||
"image_properties": {
|
||||
"original_file_name": "原始檔案名稱",
|
||||
"file_type": "檔案類型",
|
||||
"file_size": "檔案大小",
|
||||
"original_file_name": "原始文件名",
|
||||
"file_type": "文件類型",
|
||||
"file_size": "文件大小",
|
||||
"download": "下載",
|
||||
"open": "打開",
|
||||
"copy_reference_to_clipboard": "複製引用至剪貼簿",
|
||||
@@ -781,13 +781,13 @@
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "筆記ID",
|
||||
"created": "建立時間",
|
||||
"created": "新增時間",
|
||||
"modified": "修改時間",
|
||||
"type": "類型",
|
||||
"note_size": "筆記大小",
|
||||
"note_size_info": "筆記大小提供了該筆記儲存需求的粗略估計。它考慮了筆記及其歷史的內容。",
|
||||
"note_size_info": "筆記大小提供了該筆記存儲需求的粗略估計。它考慮了筆記的內容及其筆記歷史的內容。",
|
||||
"calculate": "計算",
|
||||
"subtree_size": "(子階層大小: {{size}}, 共計 {{count}} 個筆記)",
|
||||
"subtree_size": "(子階層大小: {{size}}, 共計 {{count}} 個筆記)",
|
||||
"title": "筆記資訊"
|
||||
},
|
||||
"note_map": {
|
||||
@@ -803,7 +803,7 @@
|
||||
"intro_placed": "此筆記放置在以下路徑中:",
|
||||
"intro_not_placed": "此筆記尚未放入筆記樹中。",
|
||||
"outside_hoisted": "此路徑在聚焦的筆記之外,您需要取消聚焦。",
|
||||
"archived": "已封存",
|
||||
"archived": "已歸檔",
|
||||
"search": "搜尋"
|
||||
},
|
||||
"note_properties": {
|
||||
@@ -811,7 +811,7 @@
|
||||
"info": "資訊"
|
||||
},
|
||||
"owned_attribute_list": {
|
||||
"owned_attributes": "自有屬性"
|
||||
"owned_attributes": "擁有的屬性"
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"promoted_attributes": "升級屬性",
|
||||
@@ -832,18 +832,18 @@
|
||||
},
|
||||
"search_definition": {
|
||||
"add_search_option": "新增搜尋選項:",
|
||||
"search_string": "搜尋字串",
|
||||
"search_string": "搜尋字符串",
|
||||
"search_script": "搜尋腳本",
|
||||
"ancestor": "祖先",
|
||||
"fast_search": "快速搜尋",
|
||||
"fast_search_description": "快速搜尋選項禁用筆記內容的全文搜尋,這可能會加速大資料庫中的搜尋。",
|
||||
"include_archived": "包含封存",
|
||||
"include_archived_notes_description": "封存的筆記預設不包含在搜尋結果中,使用此選項將包含它們。",
|
||||
"include_archived": "包含歸檔",
|
||||
"include_archived_notes_description": "歸檔的筆記預設不包含在搜尋結果中,使用此選項將包含它們。",
|
||||
"order_by": "排序方式",
|
||||
"limit": "限制",
|
||||
"limit_description": "限制結果數量",
|
||||
"debug": "除錯",
|
||||
"debug_description": "除錯將顯示額外的除錯資訊至控制台,以幫助除錯複雜查詢",
|
||||
"debug_description": "除錯將打印額外的除錯資訊至控制台,以幫助除錯複雜查詢",
|
||||
"action": "操作",
|
||||
"search_button": "搜尋 <kbd>Enter</kbd>",
|
||||
"search_execute": "搜尋並執行操作",
|
||||
@@ -863,11 +863,11 @@
|
||||
},
|
||||
"ancestor": {
|
||||
"label": "祖先",
|
||||
"placeholder": "依名稱搜尋筆記",
|
||||
"placeholder": "按名稱搜尋筆記",
|
||||
"depth_label": "深度",
|
||||
"depth_doesnt_matter": "任意",
|
||||
"depth_eq": "正好是 {{count}}",
|
||||
"direct_children": "直接子級",
|
||||
"direct_children": "直接下代",
|
||||
"depth_gt": "大於 {{count}}",
|
||||
"depth_lt": "小於 {{count}}"
|
||||
},
|
||||
@@ -881,14 +881,14 @@
|
||||
"description": "快速搜尋選項禁用筆記內容的全文搜尋,這可能會加快在大型資料庫中的搜尋速度。"
|
||||
},
|
||||
"include_archived_notes": {
|
||||
"include_archived_notes": "包括已封存的筆記"
|
||||
"include_archived_notes": "包括已歸檔的筆記"
|
||||
},
|
||||
"limit": {
|
||||
"limit": "限制",
|
||||
"take_first_x_results": "僅取前X個指定結果。"
|
||||
},
|
||||
"order_by": {
|
||||
"order_by": "排序方式",
|
||||
"order_by": "排序依據",
|
||||
"relevancy": "相關性(預設)",
|
||||
"title": "標題",
|
||||
"date_created": "新增日期",
|
||||
@@ -908,16 +908,16 @@
|
||||
},
|
||||
"search_script": {
|
||||
"title": "搜尋腳本:",
|
||||
"placeholder": "依名稱搜尋筆記",
|
||||
"description1": "搜尋腳本允許透過運行腳本來定義搜尋結果。這在標準搜尋不足時提供了最大的靈活性。",
|
||||
"description2": "搜尋腳本必須是類型為「程式碼」和子類型為「JavaScript後端」。腳本需要返回一個 noteIds 或 notes 數組。",
|
||||
"placeholder": "按名稱搜尋筆記",
|
||||
"description1": "搜尋腳本允許通過運行腳本來定義搜尋結果。這在標準搜尋不足時提供了最大的靈活性。",
|
||||
"description2": "搜尋腳本必須是類型為\"程式碼\"和子類型為\"JavaScript後端\"。腳本需要返回一個noteIds或notes數組。",
|
||||
"example_title": "請看這個例子:",
|
||||
"example_code": "// 1. 使用標準搜尋進行預過濾\nconst candidateNotes = api.searchForNotes(\"#journal\"); \n\n// 2. 應用自訂搜尋條件\nconst matchedNotes = candidateNotes\n .filter(note => note.title.match(/[0-9]{1,2}\\. ?[0-9]{1,2}\\. ?[0-9]{4}/));\n\nreturn matchedNotes;",
|
||||
"note": "注意,搜尋腳本和搜尋字串不能相互結合使用。"
|
||||
"note": "注意,搜尋腳本和搜尋字符串不能相互結合使用。"
|
||||
},
|
||||
"search_string": {
|
||||
"title_column": "搜尋字串:",
|
||||
"placeholder": "全文關鍵詞,#標籤 = 值…",
|
||||
"title_column": "搜尋字符串:",
|
||||
"placeholder": "全文關鍵詞,#標籤 = 值 ...",
|
||||
"search_syntax": "搜尋語法",
|
||||
"also_see": "另見",
|
||||
"complete_help": "完整的搜尋語法說明",
|
||||
@@ -929,7 +929,7 @@
|
||||
"label_year_comparison": "數字比較(也包括>,>=,<)。",
|
||||
"label_date_created": "上個月新增的筆記",
|
||||
"error": "搜尋錯誤:{{error}}",
|
||||
"search_prefix": "搜尋:"
|
||||
"search_prefix": "搜尋:"
|
||||
},
|
||||
"attachment_detail": {
|
||||
"open_help_page": "打開附件說明頁面",
|
||||
@@ -948,27 +948,27 @@
|
||||
"no_children_help": "此類型為書籍的筆記沒有任何子筆記,因此沒有內容可顯示。請參閱 <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> 以了解詳情。"
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "在這裡輸入您的程式碼筆記內容…"
|
||||
"placeholder": "在這裡輸入您的程式碼筆記內容..."
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "在這裡輸入您的筆記內容…"
|
||||
"placeholder": "在這裡輸入您的筆記內容..."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "透過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
|
||||
"search_placeholder": "依名稱搜尋筆記",
|
||||
"open_note_instruction": "通過在下面的輸入框中輸入筆記標題或在樹中選擇筆記來打開筆記。",
|
||||
"search_placeholder": "按名稱搜尋筆記",
|
||||
"enter_workspace": "進入工作區 {{title}}"
|
||||
},
|
||||
"file": {
|
||||
"file_preview_not_available": "此檔案格式不支援預覽。",
|
||||
"too_big": "預覽僅顯示檔案的前 {{maxNumChars}} 個字元以提高性能。下載檔案並在外部打開以查看完整內容。"
|
||||
"file_preview_not_available": "此文件格式不支持預覽。",
|
||||
"too_big": "預覽僅顯示文件的前 {{maxNumChars}} 個字元以提高性能。下載文件並在外部打開以查看完整內容。"
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "顯示受保護的筆記需要輸入您的密碼:",
|
||||
"start_session_button": "開始受保護的作業階段 <kbd>Enter</kbd>",
|
||||
"started": "已啟動受保護的作業階段。",
|
||||
"started": "已啟動受保護的會話。",
|
||||
"wrong_password": "密碼錯誤。",
|
||||
"protecting-finished-successfully": "已成功完成保護操作。",
|
||||
"unprotecting-finished-successfully": "已成功解除保護操作。",
|
||||
"unprotecting-finished-successfully": "已成功完成解除保護操作。",
|
||||
"protecting-in-progress": "保護進行中:{{count}}",
|
||||
"unprotecting-in-progress-count": "解除保護進行中:{{count}}",
|
||||
"protecting-title": "保護狀態",
|
||||
@@ -1007,7 +1007,7 @@
|
||||
"consistency_checks": {
|
||||
"title": "檢查一致性",
|
||||
"find_and_fix_button": "尋找並修復一致性問題",
|
||||
"finding_and_fixing_message": "正在尋找並修復一致性問題…",
|
||||
"finding_and_fixing_message": "正在尋找並修復一致性問題...",
|
||||
"issues_fixed_message": "一致性問題應該已被修復。"
|
||||
},
|
||||
"database_anonymization": {
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"full_anonymization_description": "此操作將新增一個新的資料庫副本並進行匿名化處理(刪除所有筆記內容,僅保留結構和一些非敏感元數據),用來分享至網上做除錯而無需擔心洩漏您的個人資料。",
|
||||
"save_fully_anonymized_database": "儲存完全匿名化的資料庫",
|
||||
"light_anonymization": "輕度匿名化",
|
||||
"light_anonymization_description": "此操作將新增一個新的資料庫副本,並對其進行輕度匿名化處理——僅刪除所有筆記的內容,但保留標題和屬性。此外,自訂 JS 前端 / 後端腳本筆記和自訂元件將保留。這提供了更多上下文以除錯問題。",
|
||||
"light_anonymization_description": "此操作將新增一個新的資料庫副本,並對其進行輕度匿名化處理——僅刪除所有筆記的內容,但保留標題和屬性。此外,自訂 JS 前端/後端腳本筆記和自訂小工具將保留。這提供了更多上下文以除錯問題。",
|
||||
"choose_anonymization": "您可以自行決定是提供完全匿名化還是輕度匿名化的資料庫。即使是完全匿名化的資料庫也非常有用,但在某些情況下,輕度匿名化的資料庫可以加快錯誤識別和修復的過程。",
|
||||
"save_lightly_anonymized_database": "儲存輕度匿名化的資料庫",
|
||||
"existing_anonymized_databases": "現有的匿名化資料庫",
|
||||
@@ -1040,7 +1040,7 @@
|
||||
"force_full_sync_button": "強制全量同步",
|
||||
"fill_entity_changes_button": "填充實體變更記錄",
|
||||
"full_sync_triggered": "已觸發全量同步",
|
||||
"filling_entity_changes": "正在填充實體變更列…",
|
||||
"filling_entity_changes": "正在填充實體變更列...",
|
||||
"sync_rows_filled_successfully": "同步列填充成功",
|
||||
"finished-successfully": "已完成同步。",
|
||||
"failed": "同步失敗:{{message}}"
|
||||
@@ -1049,17 +1049,17 @@
|
||||
"title": "清理資料庫",
|
||||
"description": "這會重建資料庫,通常會減少佔用空間,不會刪除數據。",
|
||||
"button_text": "清理資料庫",
|
||||
"vacuuming_database": "正在清理資料庫…",
|
||||
"vacuuming_database": "正在清理資料庫...",
|
||||
"database_vacuumed": "已清理資料庫"
|
||||
},
|
||||
"fonts": {
|
||||
"theme_defined": "跟隨主題",
|
||||
"fonts": "字型",
|
||||
"main_font": "主字型",
|
||||
"font_family": "字型",
|
||||
"font_family": "字型家族",
|
||||
"size": "大小",
|
||||
"note_tree_font": "筆記樹字型",
|
||||
"note_detail_font": "筆記內容字型",
|
||||
"note_detail_font": "筆記詳情字型",
|
||||
"monospace_font": "等寬(程式碼)字型",
|
||||
"note_tree_and_detail_font_sizing": "請注意,筆記樹字型和詳細字型的大小為相對於主字型大小設定。",
|
||||
"not_all_fonts_available": "並非所有列出的字型都在您的系統上可用。",
|
||||
@@ -1090,19 +1090,19 @@
|
||||
"disabled": "禁用"
|
||||
},
|
||||
"ribbon": {
|
||||
"widgets": "功能區元件",
|
||||
"widgets": "功能區小工具",
|
||||
"promoted_attributes_message": "如果筆記中存在升級屬性,則自動打開升級屬性功能區分頁",
|
||||
"edited_notes_message": "日記筆記中自動打開編輯過的筆記功能區分頁"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主題",
|
||||
"theme_label": "主題",
|
||||
"override_theme_fonts_label": "更改主題字型",
|
||||
"override_theme_fonts_label": "覆寫主題字型",
|
||||
"layout": "佈局",
|
||||
"layout-vertical-title": "垂直",
|
||||
"layout-horizontal-title": "水平",
|
||||
"layout-vertical-description": "啟動欄位於左側(預設)",
|
||||
"layout-horizontal-description": "啟動欄位於分頁欄下方,分頁欄現在是全寬的。",
|
||||
"layout-vertical-description": "啟動列位於左側(預設)",
|
||||
"layout-horizontal-description": "啟動列位於分頁欄下方,分頁欄現在是全寬的。",
|
||||
"auto_theme": "傳統(遵循系統配色方案)",
|
||||
"light_theme": "傳統(淺色)",
|
||||
"dark_theme": "傳統(深色)",
|
||||
@@ -1112,7 +1112,7 @@
|
||||
},
|
||||
"zoom_factor": {
|
||||
"title": "縮放係數(僅桌面版有效)",
|
||||
"description": "縮放也可以透過 CTRL+- 和 CTRL+= 快捷鍵進行控制。"
|
||||
"description": "縮放也可以通過 CTRL+- 和 CTRL+= 快捷鍵進行控制。"
|
||||
},
|
||||
"code_auto_read_only_size": {
|
||||
"title": "自動唯讀大小",
|
||||
@@ -1201,7 +1201,7 @@
|
||||
"color": "字體顏色",
|
||||
"bg_color": "背景顏色",
|
||||
"visibility_title": "高亮列表可見性",
|
||||
"visibility_description": "您可以透過添加 #hideHighlightWidget 標籤來隱藏每個筆記的高亮元件。",
|
||||
"visibility_description": "您可以透過添加 #hideHighlightWidget 標籤來隱藏每個筆記的高亮小工具。",
|
||||
"shortcut_info": "您可以在「選項」→「快捷鍵」中設定鍵盤快捷鍵快速切換右側面板(包括高亮列表)(名稱為 'toggleRightPane')。"
|
||||
},
|
||||
"table_of_contents": {
|
||||
@@ -1284,8 +1284,8 @@
|
||||
"new_password": "新密碼",
|
||||
"new_password_confirmation": "新密碼確認",
|
||||
"change_password": "更改密碼",
|
||||
"protected_session_timeout": "受保護的作業階段超時",
|
||||
"protected_session_timeout_description": "受保護的作業階段超時是一個時間段,超時後受保護的作業階段會從瀏覽器記憶體中清除。這是從最後一次存取受保護的筆記時開始計時的。更多資訊請見",
|
||||
"protected_session_timeout": "受保護的工作階段超時",
|
||||
"protected_session_timeout_description": "受保護的工作階段超時是一個時間段,超時後受保護的工作階段會從瀏覽器記憶體中清除。這是從最後一次與受保護的筆記交互開始計時的。更多資訊請見",
|
||||
"wiki": "維基",
|
||||
"for_more_info": "更多資訊。",
|
||||
"reset_confirmation": "重設密碼後將不再能查看所有目前受保護的筆記。您真的要重設密碼嗎?",
|
||||
@@ -1295,7 +1295,7 @@
|
||||
"set_password": "設定密碼",
|
||||
"password_mismatch": "新密碼不一致。",
|
||||
"password_changed_success": "密碼已更改。按 OK 後 Trilium 將重新載入。",
|
||||
"protected_session_timeout_label": "受保護的作業階段超時:"
|
||||
"protected_session_timeout_label": "受保護的工作階段超時:"
|
||||
},
|
||||
"shortcuts": {
|
||||
"keyboard_shortcuts": "快捷鍵",
|
||||
@@ -1343,8 +1343,8 @@
|
||||
"will_be_deleted_in": "此附件將在 {{time}} 後自動刪除",
|
||||
"will_be_deleted_soon": "該附件即將被自動刪除",
|
||||
"deletion_reason": ",因為該附件未連結在筆記的內容中。為防止被刪除,請將附件連結重新新增至內容中或將附件轉換為筆記。",
|
||||
"role_and_size": "角色:{{role}},大小:{{size}}",
|
||||
"link_copied": "已複製附件連結到剪貼簿。",
|
||||
"role_and_size": "角色: {{role}}, 大小: {{size}}",
|
||||
"link_copied": "附件連結已複製到剪貼簿。",
|
||||
"unrecognized_role": "無法識別的附件角色 '{{role}}'。"
|
||||
},
|
||||
"bookmark_switch": {
|
||||
@@ -1357,7 +1357,7 @@
|
||||
"read_only": "唯讀",
|
||||
"always_editable": "永遠可編輯",
|
||||
"note_is_editable": "筆記如果不太長則可編輯。",
|
||||
"note_is_read_only": "筆記為唯讀,但可以透過點擊按鈕進行編輯。",
|
||||
"note_is_read_only": "筆記為唯讀,但可以通過點擊按鈕進行編輯。",
|
||||
"note_is_always_editable": "無論筆記長度如何,永遠可編輯。"
|
||||
},
|
||||
"note-map": {
|
||||
@@ -1366,7 +1366,7 @@
|
||||
},
|
||||
"tree-context-menu": {
|
||||
"open-in-a-new-tab": "在新分頁中打開 <kbd>Ctrl+Click</kbd>",
|
||||
"open-in-a-new-split": "在新頁面分割中打開",
|
||||
"open-in-a-new-split": "在新視窗分割中打開",
|
||||
"insert-note-after": "在後面插入筆記",
|
||||
"insert-child-note": "插入子筆記",
|
||||
"delete": "刪除",
|
||||
@@ -1374,10 +1374,10 @@
|
||||
"hoist-note": "聚焦筆記",
|
||||
"unhoist-note": "取消聚焦筆記",
|
||||
"edit-branch-prefix": "編輯分支前綴",
|
||||
"advanced": "進階",
|
||||
"advanced": "高級",
|
||||
"expand-subtree": "展開子階層",
|
||||
"collapse-subtree": "收摺子階層",
|
||||
"sort-by": "排序方式…",
|
||||
"sort-by": "排序方式...",
|
||||
"recent-changes-in-subtree": "子階層中的最近更改",
|
||||
"convert-to-attachment": "轉換為附件",
|
||||
"copy-note-path-to-clipboard": "複製筆記路徑至剪貼簿",
|
||||
@@ -1393,7 +1393,7 @@
|
||||
"import-into-note": "匯入至筆記",
|
||||
"apply-bulk-actions": "套用批次操作",
|
||||
"converted-to-attachments": "{{count}} 個筆記已被轉換為附件。",
|
||||
"convert-to-attachment-confirm": "確定要將所選的筆記轉換為其父級筆記的附件嗎?",
|
||||
"convert-to-attachment-confirm": "確定要將選中的筆記轉換為其父級筆記的附件嗎?",
|
||||
"duplicate": "複製副本",
|
||||
"open-in-popup": "快速編輯"
|
||||
},
|
||||
@@ -1417,7 +1417,7 @@
|
||||
"image": "圖片",
|
||||
"launcher": "啟動器",
|
||||
"doc": "文件",
|
||||
"widget": "元件",
|
||||
"widget": "小工具",
|
||||
"confirm-change": "當筆記內容不為空時,不建議更改筆記類型。您仍然要繼續嗎?",
|
||||
"book": "集合",
|
||||
"geo-map": "地理地圖",
|
||||
@@ -1460,20 +1460,20 @@
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "快速搜尋",
|
||||
"searching": "正在搜尋…",
|
||||
"searching": "正在搜尋...",
|
||||
"no-results": "未找到結果",
|
||||
"more-results": "…以及另外 {{number}} 個結果。",
|
||||
"more-results": "... 以及另外 {{number}} 個結果。",
|
||||
"show-in-full-search": "在完整的搜尋界面中顯示"
|
||||
},
|
||||
"note_tree": {
|
||||
"collapse-title": "收摺筆記樹",
|
||||
"scroll-active-title": "捲動至使用中筆記",
|
||||
"scroll-active-title": "滾動至活動筆記",
|
||||
"tree-settings-title": "樹設定",
|
||||
"hide-archived-notes": "隱藏已封存筆記",
|
||||
"automatically-collapse-notes": "自動收摺筆記",
|
||||
"automatically-collapse-notes-title": "筆記在一段時間內未使用將被收摺,以減少樹狀結構的雜亂。",
|
||||
"save-changes": "儲存並套用更改",
|
||||
"auto-collapsing-notes-after-inactivity": "自動收摺非使用中筆記…",
|
||||
"auto-collapsing-notes-after-inactivity": "在不活動後自動收摺筆記...",
|
||||
"saved-search-note-refreshed": "已儲存的搜尋筆記已重新整理。",
|
||||
"hoist-this-note-workspace": "聚焦此筆記(工作區)",
|
||||
"refresh-saved-search-results": "重新整理儲存的搜尋結果",
|
||||
@@ -1494,7 +1494,7 @@
|
||||
"search_not_executed": "尚未執行搜尋。請點擊上方的「搜尋」按鈕查看結果。"
|
||||
},
|
||||
"spacer": {
|
||||
"configure_launchbar": "設定啟動欄"
|
||||
"configure_launchbar": "設定啟動列"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "此次查詢沒有返回任何數據"
|
||||
@@ -1510,8 +1510,8 @@
|
||||
"close_right_tabs": "關閉右側分頁",
|
||||
"close_all_tabs": "關閉所有分頁",
|
||||
"reopen_last_tab": "重新打開最後關閉的分頁",
|
||||
"move_tab_to_new_window": "將此分頁移動至新視窗",
|
||||
"copy_tab_to_new_window": "將此分頁複製至新視窗",
|
||||
"move_tab_to_new_window": "將此分頁移動至新窗口",
|
||||
"copy_tab_to_new_window": "將此分頁複製至新窗口",
|
||||
"new_tab": "新分頁"
|
||||
},
|
||||
"toc": {
|
||||
@@ -1519,8 +1519,8 @@
|
||||
"options": "選項"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "檔案 <code class=\"file-path\"></code> 最後修改時間為 <span class=\"file-last-modified\"></span>。",
|
||||
"upload_modified_file": "上傳修改的檔案",
|
||||
"file_last_modified": "文件 <code class=\"file-path\"></code> 最後修改時間為 <span class=\"file-last-modified\"></span>。",
|
||||
"upload_modified_file": "上傳修改的文件",
|
||||
"ignore_this_change": "忽略此更改"
|
||||
},
|
||||
"app_context": {
|
||||
@@ -1553,7 +1553,7 @@
|
||||
"undeleting-notes-finished-successfully": "已成功還原刪除的筆記。"
|
||||
},
|
||||
"frontend_script_api": {
|
||||
"async_warning": "您正在將一個異步函數傳遞給 `api.runOnBackend()`,這可能讓功能無法按預期工作。\\n請將該函數同步(透過移除 `async` 關鍵字),或使用 `api.runAsyncOnBackendWithManualTransactionHandling()`。",
|
||||
"async_warning": "您正在將一個異步函數傳遞給 `api.runOnBackend()`,這可能無法按預期工作。\\n請使該函數同步(透過移除 `async` 關鍵字),或使用 `api.runAsyncOnBackendWithManualTransactionHandling()`。",
|
||||
"sync_warning": "您正在將一個同步函數傳遞給 `api.runAsyncOnBackendWithManualTransactionHandling()`,\\n而您可能應該使用 `api.runOnBackend()`。"
|
||||
},
|
||||
"ws": {
|
||||
@@ -1565,11 +1565,11 @@
|
||||
"confirm_unhoisting": "請求的筆記 '{{requestedNote}}' 位於聚焦的筆記 '{{hoistedNote}}' 的子階層之外,您必須取消聚焦才能訪問該筆記。是否繼續取消聚焦?"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"reset_launcher_confirm": "您確定要重設 \"{{title}}\" 嗎?此筆記(及其子項)中的所有數據及設定將丟失,且啟動器將還原至其原始位置。",
|
||||
"reset_launcher_confirm": "您確定要重設 \"{{title}}\" 嗎?此筆記(及其子項)中的所有數據/設定將丟失,且啟動器將還原至其原始位置。",
|
||||
"add-note-launcher": "添加筆記啟動器",
|
||||
"add-script-launcher": "添加腳本啟動器",
|
||||
"add-custom-widget": "添加自訂元件",
|
||||
"add-spacer": "添加分隔元件",
|
||||
"add-custom-widget": "添加自訂小工具",
|
||||
"add-spacer": "添加空白占位",
|
||||
"delete": "刪除 <kbd data-command=\"deleteNotes\"></kbd>",
|
||||
"reset": "重設",
|
||||
"move-to-visible-launchers": "移動至可見啟動器",
|
||||
@@ -1626,8 +1626,8 @@
|
||||
},
|
||||
"link_context_menu": {
|
||||
"open_note_in_new_tab": "在新分頁中打開筆記",
|
||||
"open_note_in_new_split": "在新頁面分割中打開筆記",
|
||||
"open_note_in_new_window": "在新視窗中打開筆記",
|
||||
"open_note_in_new_split": "在新視窗分割中打開筆記",
|
||||
"open_note_in_new_window": "在新窗口中打開筆記",
|
||||
"open_note_in_popup": "快速編輯"
|
||||
},
|
||||
"zen_mode": {
|
||||
@@ -1778,12 +1778,12 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "筆記歷史快照間隔",
|
||||
"note_revisions_snapshot_description": "筆記歷史快照間隔是建立新筆記修訂的時間。如需詳細資訊,請參閱 <doc>wiki</doc>。",
|
||||
"note_revisions_snapshot_description": "筆記歷史快照間隔是建立新筆記修訂的時間。如需詳細資訊,請參閱 <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"snapshot_time_interval_label": "筆記歷史快照時間間隔:"
|
||||
},
|
||||
"custom_date_time_format": {
|
||||
"title": "自訂日期/時間格式",
|
||||
"description": "透過 <shortcut /> 或工具列自訂插入日期和時間的格式。有關可用的格式及符號,請參閱 <doc>Day.js docs</doc>。",
|
||||
"description": "透過 <kbd></kbd> 或工具列自訂插入日期和時間的格式。有關可用的格式及符號,請參閱 <a href=\"https://day.js.org/docs/en/display/format\" target=\"_blank\" rel=\"noopener noreferrer\">Day.js docs</a>。",
|
||||
"format_string": "格式化字串:",
|
||||
"formatted_time": "格式化日期/時間:"
|
||||
},
|
||||
@@ -1814,7 +1814,7 @@
|
||||
"recovery_keys_unused": "復原碼 {{index}} 未使用",
|
||||
"oauth_title": "OAuth/OpenID 驗證",
|
||||
"oauth_description": "OpenID 是一種標準化的方式,可讓您使用其他服務(如 Google)的帳號登入網站,以驗證您的身份。預設的提供者是 Google,但您可以將其變更為任何其他 OpenID 提供者。查看<a href=\"#root/_hidden/_help/_help_Otzi9La2YAUX/_help_WOcw2SLH6tbX/_help_7DAiwaf8Z7Rz\">此處</a>以瞭解更多資訊。依照這些 <a href=\"https://developers.google.com/identity/openid-connect/openid-connect\">指示</a>以透過 Google 設定 OpenID 服務。",
|
||||
"oauth_description_warning": "要啟用 OAuth / OpenID,您需要設定 config.ini 文件中的 OAuth / OpenID 基礎 URL、客戶端 ID 和客戶端金鑰,並重新啟動應用程式。如果要從環境變數設置,請設定 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 環境變數。",
|
||||
"oauth_description_warning": "要啟用 OAuth/OpenID,您需要設置 config.ini 文件中的 OAuth/OpenID 基礎 URL、用戶端 ID 和客戶端金鑰,並重新啟動應用程式。如果要從環境變數設置,請設置 TRILIUM_OAUTH_BASE_URL、TRILIUM_OAUTH_CLIENT_ID 和 TRILIUM_OAUTH_CLIENT_SECRET 環境變數。",
|
||||
"oauth_missing_vars": "缺少以下設定:{{variables}}",
|
||||
"oauth_user_account": "用戶帳號: ",
|
||||
"oauth_user_email": "用戶信箱: ",
|
||||
@@ -1883,7 +1883,7 @@
|
||||
}
|
||||
},
|
||||
"content_widget": {
|
||||
"unknown_widget": "未知元件:\"{{id}}\"。"
|
||||
"unknown_widget": "未知小工具:\"{{id}}\"。"
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "不設定",
|
||||
@@ -1977,27 +1977,26 @@
|
||||
"show_attachments_description": "查看筆記附件",
|
||||
"search_notes_title": "搜尋筆記",
|
||||
"search_notes_description": "打開進階搜尋",
|
||||
"search_subtree_title": "在子階層中搜尋",
|
||||
"search_subtree_description": "在目前子階層中搜尋",
|
||||
"search_subtree_title": "在子樹中搜尋",
|
||||
"search_subtree_description": "在目前子樹中搜尋",
|
||||
"search_history_title": "顯示搜尋歷史",
|
||||
"search_history_description": "查看搜尋記錄",
|
||||
"configure_launch_bar_title": "設定啟動欄",
|
||||
"configure_launch_bar_description": "打開啟動欄設定以新增或移除項目。"
|
||||
"configure_launch_bar_title": "設定啟動列",
|
||||
"configure_launch_bar_description": "打開啟動列設定以新增或移除項目。"
|
||||
},
|
||||
"content_renderer": {
|
||||
"open_externally": "以外部程式打開"
|
||||
"open_externally": "在外部打開"
|
||||
},
|
||||
"modal": {
|
||||
"close": "關閉",
|
||||
"help_title": "顯示關於此畫面的更多資訊"
|
||||
},
|
||||
"call_to_action": {
|
||||
"next_theme_title": "新的 Trilium 主題已推出穩定版本",
|
||||
"next_theme_message": "有一段時間,我們一直在設計新的主題,為了讓應用程式看起來更現代。",
|
||||
"next_theme_button": "切換至新的 Trilium 主題",
|
||||
"background_effects_title": "背景特效已推出穩定版本",
|
||||
"background_effects_message": "在 Windows 裝置上,背景特效現在已完全穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。此技術也用於其他應用程式,例如 Windows 檔案總管。",
|
||||
"background_effects_button": "啟用背景特效",
|
||||
"next_theme_title": "試用新 Trilium 主題",
|
||||
"next_theme_message": "您正在使用舊版主題,要試用新主題嗎?",
|
||||
"next_theme_button": "試用新主題",
|
||||
"dismiss": "關閉"
|
||||
"background_effects_button": "啟用背景特效"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
{
|
||||
"add_link": {
|
||||
"add_link": "Додати посилання",
|
||||
"help_on_links": "Довідка щодо посилань",
|
||||
"note": "Нотатка",
|
||||
"search_note": "пошук нотатки за її назвою",
|
||||
"link_title_mirrors": "заголовок посилання відображає поточний заголовок нотатки",
|
||||
"link_title_arbitrary": "заголовок посилання можна змінювати довільно",
|
||||
"link_title": "Заголовок посилання",
|
||||
"button_add_link": "Додати посилання"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"save": "Зберегти",
|
||||
"edit_branch_prefix": "Редагувати префікс гілки",
|
||||
"help_on_tree_prefix": "Довідка щодо префіксів гілок",
|
||||
"prefix": "Префікс: ",
|
||||
"branch_prefix_saved": "Префікс гілки збережено."
|
||||
},
|
||||
"about": {
|
||||
"app_version": "Версія програми:",
|
||||
"db_version": "Версія БД:",
|
||||
"build_date": "Дата збірки:",
|
||||
"build_revision": "Ревізія збірки:",
|
||||
"data_directory": "Каталог даних:",
|
||||
"homepage": "Домашня сторінка:",
|
||||
"title": "Про Trilium Notes",
|
||||
"sync_version": "Версія синхронізації:"
|
||||
},
|
||||
"global_menu": {
|
||||
"about": "Про Trilium Notes"
|
||||
},
|
||||
"modal": {
|
||||
"help_title": "Показати більше інформації про це вікно"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Критична помилка",
|
||||
"message": "Сталася критична помилка, яка перешкоджає запуску клієнтської програми:\n\n{{message}}\n\nНайімовірніше, це спричинено несподіваною помилкою скрипту. Спробуйте запустити програму в безпечному режимі та вирішити проблему."
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Помилка ініціалізації віджета",
|
||||
"message-custom": "Не вдалося ініціалізувати користувацький віджет із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" через:\n\n{{message}}",
|
||||
"message-unknown": "Невідомий віджет не вдалося ініціалізувати через:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Не вдалося завантажити власний скрипт",
|
||||
"message": "Скрипт із нотатки з ID \"{{id}}\" під назвою \"{{title}}\" не вдалося виконати через:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Масові дії",
|
||||
"affected_notes": "Застосовані нотатки",
|
||||
"available_actions": "Доступні дії",
|
||||
"chosen_actions": "Обрані дії",
|
||||
"execute_bulk_actions": "Виконання масових дій",
|
||||
"bulk_actions_executed": "Масові дії успішно виконано.",
|
||||
"none_yet": "Поки що немає... додайте дію, натиснувши одну з доступних вище.",
|
||||
"include_descendants": "Включити нащадків вибраних нотаток",
|
||||
"labels": "Мітки",
|
||||
"relations": "Відносини",
|
||||
"notes": "Нотатки",
|
||||
"other": "Інше"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Клонувати нотатки до...",
|
||||
"target_parent_note": "Цільова батьківська нотатка",
|
||||
"search_for_note_by_its_name": "Знайти нотатку за назвою",
|
||||
"help_on_links": "Допомога з посиланнями",
|
||||
"notes_to_clone": "Нотатки для клонування",
|
||||
"cloned_note_prefix_title": "Клонована нотатка буде відображатися в дереві нотаток із заданим префіксом",
|
||||
"prefix_optional": "Префікс (необов'язково)",
|
||||
"clone_to_selected_note": "Клонувати до вибраної нотатки",
|
||||
"no_path_to_clone_to": "Немає шляху для клонування.",
|
||||
"note_cloned": "Нотатку \"{{clonedTitle}}\" було клоновано в \"{{targetTitle}}\""
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Нотатку(-и) було скопійовано в буфер.",
|
||||
"copy_failed": "Не вдалося скопіювати в буфер через проблеми з дозволами.",
|
||||
"copy_success": "Скопійовано в буфер."
|
||||
},
|
||||
"entrypoints": {
|
||||
"sql-error": "Виникла помилка при виконанні запиту SQL: {{message}}"
|
||||
},
|
||||
"branches": {
|
||||
"undeleting-notes-finished-successfully": "Нотатки вдало відновлено.",
|
||||
"undeleting-notes-in-progress": "Відновлюємо нотатки: {{count}}",
|
||||
"delete-notes-in-progress": "Видаляємо нотатки: {{count}}",
|
||||
"delete-finished-successfully": "Нотатки вдало видалено."
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"add-spacer": "Додати розділювач",
|
||||
"reset": "Скинути"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Автовизначена"
|
||||
},
|
||||
"highlighting": {
|
||||
"color-scheme": "Схема кольорів"
|
||||
},
|
||||
"code_block": {
|
||||
"copy_title": "Скопіювати в буфер"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Форматування"
|
||||
},
|
||||
"editor": {
|
||||
"title": "Редактор"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Панель інструментів форматування"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Підтвердження",
|
||||
"cancel": "Скасувати",
|
||||
"ok": "ОК",
|
||||
"are_you_sure_remove_note": "Ви впевнені, що хочете видалити нотатку \"{{title}}\" з карти зв'язків? ",
|
||||
"if_you_dont_check": "Якщо ви не позначите цей пункт, нотатку буде видалено лише з карти зв'язків.",
|
||||
"also_delete_note": "Також видалити нотатку"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "Видалити попередній перегляд нотаток",
|
||||
"close": "Закрити",
|
||||
"delete_all_clones_description": "Видалити також усі клони (можна скасувати в останніх змінах)",
|
||||
"erase_notes_description": "Звичайне (м’яке) видалення лише позначає нотатки як видалені, і їх можна відновити (у діалоговому вікні останніх змін) протягом певного періоду часу. Якщо позначити цю опцію, нотатки будуть видалені негайно, і їх неможливо буде відновити.",
|
||||
"erase_notes_warning": "Стерти нотатки назавжди (скасувати не можна), включаючи всі клони. Це призведе до перезавантаження програми.",
|
||||
"notes_to_be_deleted": "Наступні нотатки будуть видалені ({{notesCount}})",
|
||||
"no_note_to_delete": "Жодну нотатку не буде видалено (лише клони).",
|
||||
"broken_relations_to_be_deleted": "Наступні зв'язки будуть розірвані та видалені ({{ relationCount}})",
|
||||
"cancel": "Скасувати",
|
||||
"ok": "ОК",
|
||||
"deleted_relation_text": "Нотатка {{- note}} (буде видалена) посилається на зв'язок {{- relation}}, що походить з {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "Експорт нотатки",
|
||||
"close": "Закрити",
|
||||
"export_type_subtree": "Ця нотатка та всі її нащадки",
|
||||
"format_html": "HTML – рекомендовано, оскільки він зберігає всі формати",
|
||||
"format_html_zip": "HTML у ZIP-архіві – це рекомендовано, оскільки це зберігає все форматування.",
|
||||
"format_markdown": "Markdown – це зберігає більшу частину форматування.",
|
||||
"format_opml": "OPML – формат обміну структурами лише для тексту. Форматування, зображення та файли не включено.",
|
||||
"opml_version_1": "OPML версії 1.0 – лише звичайний текст",
|
||||
"opml_version_2": "OPML v2.0 - також дозволяє HTML",
|
||||
"export_type_single": "Тільки ця нотатка без її нащадків",
|
||||
"export": "Експорт",
|
||||
"choose_export_type": "Спочатку виберіть тип експорту",
|
||||
"export_status": "Стан експорту",
|
||||
"export_in_progress": "Триває експорт: {{progressCount}}",
|
||||
"export_finished_successfully": "Експорт успішно завершено.",
|
||||
"format_pdf": "PDF – для друку або спільного використання."
|
||||
},
|
||||
"help": {
|
||||
"title": "Шпаргалка",
|
||||
"noteNavigation": "Навігація по нотатках",
|
||||
"goUpDown": "переміститись вгору/вниз у списку нотаток",
|
||||
"collapseExpand": "згорнути/розгорнути вузол",
|
||||
"notSet": "не встановлено",
|
||||
"goBackForwards": "повернутися назад / вперед в історії",
|
||||
"showJumpToNoteDialog": "показати <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">діалогове вікно \"Перейти до\"</a>",
|
||||
"scrollToActiveNote": "прокрутити до активної нотатки",
|
||||
"jumpToParentNote": "перейти до батьківської нотатки",
|
||||
"collapseWholeTree": "згорнути все дерево нотаток",
|
||||
"collapseSubTree": "згорнути піддерево",
|
||||
"tabShortcuts": "Швидкі клавіші вкладки",
|
||||
"newTabNoteLink": "посилання на нотатку відкриває нотатку в новій вкладці",
|
||||
"newTabWithActivationNoteLink": "посилання на нотатку відкривається та активує нотатку в новій вкладці",
|
||||
"onlyInDesktop": "Тільки для ПК (збірка Electron)",
|
||||
"openEmptyTab": "відкрити порожню вкладку",
|
||||
"closeActiveTab": "закрити активну вкладку",
|
||||
"activateNextTab": "активувати наступну вкладку",
|
||||
"activatePreviousTab": "активувати попередню вкладку",
|
||||
"creatingNotes": "Створення нотаток",
|
||||
"createNoteAfter": "створити нову нотатку після активної нотатки",
|
||||
"createNoteInto": "створити нову піднотатку в активній нотатці",
|
||||
"editBranchPrefix": "редагувати <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">префікс</a> активного клону нотатки",
|
||||
"movingCloningNotes": "Переміщення / клонування нотаток",
|
||||
"moveNoteUpDown": "переміщення нотатки вгору/вниз у списку нотаток",
|
||||
"moveNoteUpHierarchy": "перемістити нотатку вище в ієрархії",
|
||||
"multiSelectNote": "множинний вибір нотатки вище/нижче",
|
||||
"selectAllNotes": "вибрати всі нотатки на поточному рівні",
|
||||
"selectNote": "вибрати нотатку",
|
||||
"copyNotes": "копіювати активну нотатку (або поточний вибір) у буфер обміну (використовується для <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/cloning-notes.html#cloning-notes\">клонування</a>)",
|
||||
"cutNotes": "вирізати поточну нотатку (або поточний вибір) у буфер обміну (використовується для переміщення нотаток)",
|
||||
"pasteNotes": "вставити нотатку(и) як піднотатку в активну нотатку (яка або переміщується, або клонується залежно від того, чи була вона скопійована, чи вирізана в буфер обміну)",
|
||||
"deleteNotes": "видалити нотатку / піддерево",
|
||||
"editingNotes": "Редагування нотаток",
|
||||
"editNoteTitle": "На панелі дерева перемкнеться з панелі дерева на назву нотатки. Введення з назви нотатки перемкне фокус на текстовий редактор. <kbd>Ctrl+.</kbd> перемкнеться назад з редактора на панель дерева.",
|
||||
"createEditLink": "створити / редагувати зовнішнє посилання",
|
||||
"createInternalLink": "створити внутрішнє посилання",
|
||||
"followLink": "перейти за посиланням під курсором",
|
||||
"insertDateTime": "вставити поточну дату та час у позицію курсору",
|
||||
"jumpToTreePane": "перейти до панелі дерева та прокрутити до активної нотатки",
|
||||
"markdownAutoformat": "Автоформатування, подібне до Markdown",
|
||||
"headings": "<code>##</code>, <code>###</code>, <code>####</code> тощо, а потім пробіл для заголовків",
|
||||
"bulletList": "<code>*</code> або <code>-</code> з пробілом для маркованого списку",
|
||||
"numberedList": "<code>1.</code> або <code>1)</code>, а потім пробіл для нумерованого списку",
|
||||
"blockQuote": "починайте рядок з <code>></code>, а потім пробіл для цитування блоку",
|
||||
"troubleshooting": "Усунення несправностей",
|
||||
"reloadFrontend": "перезавантажити інтерфейс Trilium",
|
||||
"showDevTools": "показати інструменти розробника",
|
||||
"showSQLConsole": "показати консоль SQL",
|
||||
"other": "Інше",
|
||||
"quickSearch": "зосередження на швидкому введенні пошукового запиту",
|
||||
"inPageSearch": "пошук на сторінці"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Імпортувати в нотатку",
|
||||
"chooseImportFile": "Вибрати файл імпорту",
|
||||
"importDescription": "Вміст вибраного(их) файлу(ів) буде імпортовано як дочірню(і) нотатку(и) до",
|
||||
"options": "Опції",
|
||||
"safeImportTooltip": "Експортовані файли Trilium <code>.zip</code> можуть містити виконувані скрипти, які можуть мати шкідливу поведінку. Безпечний імпорт деактивує автоматичне виконання всіх імпортованих скриптів. Зніміть позначку \"Безпечний імпорт\", лише якщо імпортований архів має містити виконувані скрипти, і ви повністю довіряєте вмісту файлу імпорту.",
|
||||
"safeImport": "Безпечний імпорт",
|
||||
"explodeArchivesTooltip": "Якщо цей прапорець позначено, Trilium читатиме файли <code>.zip</code>, <code>.enex</code> та <code>.opml</code> і створюватиме нотатки з файлів усередині цих архівів. Якщо прапорець знято, Trilium додаватиме самі архіви до нотатки.",
|
||||
"explodeArchives": "Зчитати вміст архівів <code>.zip</code>, <code>.enex</code> та <code>.opml</code>.",
|
||||
"shrinkImagesTooltip": "<p>Якщо ви позначите цей параметр, Trilium спробує зменшити імпортовані зображення шляхом масштабування та оптимізації, що може вплинути на сприйняту якість зображення. Якщо не позначити, зображення будуть імпортовані без змін.</p><p>Це не стосується імпорту <code>.zip</code> з метаданими, оскільки передбачається, що ці файли вже оптимізовані.</p>",
|
||||
"shrinkImages": "Зменшити зображення",
|
||||
"textImportedAsText": "Імпортувати HTML, Markdown та TXT як текстові нотатки, якщо це незрозуміло з метаданих",
|
||||
"codeImportedAsCode": "Імпортувати розпізнані файли коду (наприклад, <code>.json</code>) як нотатки до коду, якщо це незрозуміло з метаданих",
|
||||
"replaceUnderscoresWithSpaces": "Замінити підкреслення пробілами в назвах імпортованих нотаток",
|
||||
"import": "Імпорт",
|
||||
"failed": "Помилка імпорту: {{message}}."
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Підказка",
|
||||
"ok": "ОК",
|
||||
"defaultTitle": "Підказка"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"modal_title": "Захищений сеанс",
|
||||
"help_title": "Довідка щодо захищених нотаток",
|
||||
"close_label": "Закрити",
|
||||
"form_label": "Щоб продовжити запитувану дію, вам потрібно розпочати захищений сеанс, ввівши пароль:",
|
||||
"start_button": "Розпочати захищений сеанс"
|
||||
},
|
||||
"recent_changes": {
|
||||
"title": "Останні зміни",
|
||||
"erase_notes_button": "Стерти видалені нотатки зараз",
|
||||
"deleted_notes_message": "Видалені нотатки стерто.",
|
||||
"no_changes_message": "Поки що жодних змін...",
|
||||
"undelete_link": "відновити",
|
||||
"confirm_undelete": "Ви хочете відновити цю нотатку та її піднотатки?"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "Зміни нотаток",
|
||||
"delete_all_revisions": "Видалити всі редакції цієї нотатки"
|
||||
}
|
||||
}
|
||||
@@ -65,16 +65,5 @@
|
||||
},
|
||||
"rename_note": {
|
||||
"rename_note": "Đổi tên ghi chép"
|
||||
},
|
||||
"add_label": {
|
||||
"add_label": "Thêm nhãn",
|
||||
"label_name_placeholder": "tên nhãn",
|
||||
"help_text_item2": "hoặc thay đổi giá trị của nhãn có sẵn"
|
||||
},
|
||||
"rename_label": {
|
||||
"rename_label": "Đặt lại tên nhãn"
|
||||
},
|
||||
"call_to_action": {
|
||||
"dismiss": "Bỏ qua"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputName = this.$widget.find(".attr-input-name");
|
||||
this.$inputName.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
@@ -383,7 +383,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputValue = this.$widget.find(".attr-input-value");
|
||||
this.$inputValue.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
@@ -421,7 +421,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
this.$inputInverseRelation = this.$widget.find(".attr-input-inverse-relation");
|
||||
this.$inputInverseRelation.on("input", (ev) => {
|
||||
if (!(ev.originalEvent as KeyboardEvent)?.isComposing) {
|
||||
// https://github.com/TriliumNext/Trilium/pull/3812
|
||||
// https://github.com/zadam/trilium/pull/3812
|
||||
this.userEditedAttribute();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -173,7 +173,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
this.attributeDetailWidget.hide();
|
||||
});
|
||||
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/TriliumNext/Trilium/issues/4160
|
||||
this.$editor.on("blur", () => setTimeout(() => this.save(), 100)); // Timeout to fix https://github.com/zadam/trilium/issues/4160
|
||||
|
||||
this.$addNewAttributeButton = this.$widget.find(".add-new-attribute-button");
|
||||
this.$addNewAttributeButton.on("click", (e) => this.addNewAttribute(e));
|
||||
@@ -282,7 +282,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
|
||||
|
||||
async save() {
|
||||
if (this.lastUpdatedNoteId !== this.noteId) {
|
||||
// https://github.com/TriliumNext/Trilium/issues/3090
|
||||
// https://github.com/zadam/trilium/issues/3090
|
||||
console.warn("Ignoring blur event because a different note is loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -414,7 +414,7 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
async fetchLatestVersion() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Notes/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -2,7 +2,6 @@ import FlexContainer from "./flex_container.js";
|
||||
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import type NoteContext from "../../components/note_context.js";
|
||||
import Component from "../../components/component.js";
|
||||
|
||||
interface NoteContextEvent {
|
||||
noteContext: NoteContext;
|
||||
@@ -153,8 +152,6 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
for (const ntxId of ntxIds) {
|
||||
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove();
|
||||
|
||||
const widget = this.widgets[ntxId];
|
||||
recursiveCleanup(widget);
|
||||
delete this.widgets[ntxId];
|
||||
}
|
||||
}
|
||||
@@ -240,12 +237,3 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
function recursiveCleanup(widget: Component) {
|
||||
for (const child of widget.children) {
|
||||
recursiveCleanup(child);
|
||||
}
|
||||
if ("cleanup" in widget && typeof widget.cleanup === "function") {
|
||||
widget.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ function AddLinkDialogComponent() {
|
||||
}}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("add_link.note")} name="note">
|
||||
<FormGroup label={t("add_link.note")}>
|
||||
<NoteAutocomplete
|
||||
inputRef={autocompleteRef}
|
||||
onChange={setSuggestion}
|
||||
|
||||
@@ -64,7 +64,7 @@ function BranchPrefixDialogComponent() {
|
||||
footer={<Button text={t("branch_prefix.save")} />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup label={t("branch_prefix.prefix")} name="prefix">
|
||||
<FormGroup label={t("branch_prefix.prefix")}>
|
||||
<div class="input-group">
|
||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||
|
||||
@@ -94,8 +94,7 @@ function AvailableActionsList() {
|
||||
<td>{ actionGroup.title }:</td>
|
||||
{actionGroup.actions.map(({ actionName, actionTitle }) =>
|
||||
<Button
|
||||
size="small"
|
||||
text={actionTitle}
|
||||
small text={actionTitle}
|
||||
onClick={() => bulk_action.addAction("_bulkAction", actionName)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import Button from "../react/Button";
|
||||
import Modal from "../react/Modal";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
import { CallToAction, dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActions: CallToAction[] }) {
|
||||
if (!activeCallToActions.length) {
|
||||
@@ -26,12 +25,12 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
<Modal
|
||||
className="call-to-action"
|
||||
size="md"
|
||||
title={activeItem.title}
|
||||
title="New features"
|
||||
show={shown}
|
||||
onHidden={() => setShown(false)}
|
||||
footerAlignment="between"
|
||||
footer={<>
|
||||
<Button text={t("call_to_action.dismiss")} onClick={async () => {
|
||||
<Button text="Dismiss" onClick={async () => {
|
||||
await dismissCallToAction(activeItem.id);
|
||||
goToNext();
|
||||
}} />
|
||||
@@ -44,6 +43,7 @@ function CallToActionDialogComponent({ activeCallToActions }: { activeCallToActi
|
||||
)}
|
||||
</>}
|
||||
>
|
||||
<h4>{activeItem.title}</h4>
|
||||
<p>{activeItem.message}</p>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ const CALL_TO_ACTIONS: CallToAction[] = [
|
||||
id: "background_effects",
|
||||
title: t("call_to_action.background_effects_title"),
|
||||
message: t("call_to_action.background_effects_message"),
|
||||
enabled: () => false,
|
||||
enabled: () => utils.isElectron() && window.glob.platform === "win32" && isNextTheme() && !options.is("backgroundEffects"),
|
||||
buttons: [
|
||||
{
|
||||
text: t("call_to_action.background_effects_button"),
|
||||
|
||||
@@ -69,15 +69,15 @@ function CloneToDialogComponent() {
|
||||
>
|
||||
<h5>{t("clone_to.notes_to_clone")}</h5>
|
||||
<NoteList style={{ maxHeight: "200px", overflow: "auto" }} noteIds={clonedNoteIds} />
|
||||
<FormGroup name="target-parent-note" label={t("clone_to.target_parent_note")}>
|
||||
<FormGroup label={t("clone_to.target_parent_note")}>
|
||||
<NoteAutocomplete
|
||||
placeholder={t("clone_to.search_for_note_by_its_name")}
|
||||
onChange={setSuggestion}
|
||||
inputRef={autoCompleteRef}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup name="clone-prefix" label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
|
||||
<FormTextBox onChange={setPrefix} />
|
||||
<FormGroup label={t("clone_to.prefix_optional")} title={t("clone_to.cloned_note_prefix_title")}>
|
||||
<FormTextBox name="clone-prefix" onChange={setPrefix} />
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import tree from "../../services/tree";
|
||||
import Button from "../react/Button";
|
||||
import FormCheckbox from "../react/FormCheckbox";
|
||||
import FormFileUpload from "../react/FormFileUpload";
|
||||
import FormGroup, { FormMultiGroup } from "../react/FormGroup";
|
||||
import FormGroup from "../react/FormGroup";
|
||||
import Modal from "../react/Modal";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import ReactBasicWidget from "../react/ReactBasicWidget";
|
||||
@@ -55,11 +55,11 @@ function ImportDialogComponent() {
|
||||
footer={<Button text={t("import.import")} primary disabled={!files} />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup name="files" label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
|
||||
<FormGroup label={t("import.chooseImportFile")} description={<>{t("import.importDescription")} <strong>{ noteTitle }</strong></>}>
|
||||
<FormFileUpload multiple onChange={setFiles} />
|
||||
</FormGroup>
|
||||
|
||||
<FormMultiGroup label={t("import.options")}>
|
||||
<FormGroup label={t("import.options")}>
|
||||
<FormCheckbox
|
||||
name="safe-import" hint={t("import.safeImportTooltip")} label={t("import.safeImport")}
|
||||
currentValue={safeImport} onChange={setSafeImport}
|
||||
@@ -84,7 +84,7 @@ function ImportDialogComponent() {
|
||||
name="replace-underscores-with-spaces" label={t("import.replaceUnderscoresWithSpaces")}
|
||||
currentValue={replaceUnderscoresWithSpaces} onChange={setReplaceUnderscoresWithSpaces}
|
||||
/>
|
||||
</FormMultiGroup>
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ function IncludeNoteDialogComponent() {
|
||||
footer={<Button text={t("include_note.button_include")} keyboardShortcut="Enter" />}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup name="note" label={t("include_note.label_note")}>
|
||||
<FormGroup label={t("include_note.label_note")}>
|
||||
<NoteAutocomplete
|
||||
placeholder={t("include_note.placeholder_search")}
|
||||
onChange={setSuggestion}
|
||||
@@ -55,9 +55,8 @@ function IncludeNoteDialogComponent() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="include-note-box-size" label={t("include_note.box_size_prompt")}>
|
||||
<FormRadioGroup
|
||||
name="include-note-box-size"
|
||||
<FormGroup label={t("include_note.box_size_prompt")}>
|
||||
<FormRadioGroup name="include-note-box-size"
|
||||
currentValue={boxSize} onChange={setBoxSize}
|
||||
values={[
|
||||
{ label: t("include_note.box_size_small"), value: "small" },
|
||||
|
||||
@@ -57,7 +57,7 @@ function MoveToDialogComponent() {
|
||||
<h5>{t("move_to.notes_to_move")}</h5>
|
||||
<NoteList branchIds={movedBranchIds} />
|
||||
|
||||
<FormGroup name="parent-note" label={t("move_to.target_parent_note")}>
|
||||
<FormGroup label={t("move_to.target_parent_note")}>
|
||||
<NoteAutocomplete
|
||||
onChange={setSuggestion}
|
||||
inputRef={autoCompleteRef}
|
||||
|
||||
@@ -83,7 +83,7 @@ function NoteTypeChooserDialogComponent() {
|
||||
show={shown}
|
||||
stackable
|
||||
>
|
||||
<FormGroup name="parent-note" label={t("note_type_chooser.change_path_prompt")}>
|
||||
<FormGroup label={t("note_type_chooser.change_path_prompt")}>
|
||||
<NoteAutocomplete
|
||||
onChange={setParentNote}
|
||||
placeholder={t("note_type_chooser.search_placeholder")}
|
||||
@@ -95,7 +95,7 @@ function NoteTypeChooserDialogComponent() {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="note-type" label={t("note_type_chooser.modal_body")}>
|
||||
<FormGroup label={t("note_type_chooser.modal_body")}>
|
||||
<FormList onSelect={onNoteTypeSelected}>
|
||||
{noteTypes.map((_item) => {
|
||||
if (_item.title === "----") {
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface PromptDialogOptions {
|
||||
defaultValue?: string;
|
||||
shown?: PromptShownDialogCallback;
|
||||
callback?: (value: string | null) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
function PromptDialogComponent() {
|
||||
@@ -33,26 +32,24 @@ function PromptDialogComponent() {
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
const answerRef = useRef<HTMLInputElement>(null);
|
||||
const opts = useRef<PromptDialogOptions>();
|
||||
const [ opts, setOpts ] = useState<PromptDialogOptions>();
|
||||
const [ value, setValue ] = useState("");
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const submitValue = useRef<string>(null);
|
||||
|
||||
useTriliumEvent("showPromptDialog", (newOpts) => {
|
||||
opts.current = newOpts;
|
||||
setValue(newOpts.defaultValue ?? "");
|
||||
useTriliumEvent("showPromptDialog", (opts) => {
|
||||
setOpts(opts);
|
||||
setShown(true);
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="prompt-dialog"
|
||||
title={opts.current?.title ?? t("prompt.title")}
|
||||
title={opts?.title ?? t("prompt.title")}
|
||||
size="lg"
|
||||
zIndex={2000}
|
||||
modalRef={modalRef} formRef={formRef}
|
||||
onShown={() => {
|
||||
opts.current?.shown?.({
|
||||
opts?.shown?.({
|
||||
$dialog: refToJQuerySelector(modalRef),
|
||||
$question: refToJQuerySelector(labelRef),
|
||||
$answer: refToJQuerySelector(answerRef),
|
||||
@@ -61,25 +58,24 @@ function PromptDialogComponent() {
|
||||
answerRef.current?.focus();
|
||||
}}
|
||||
onSubmit={() => {
|
||||
submitValue.current = value;
|
||||
setShown(false);
|
||||
const modal = BootstrapModal.getOrCreateInstance(modalRef.current!);
|
||||
modal.hide();
|
||||
|
||||
opts?.callback?.(value);
|
||||
}}
|
||||
onHidden={() => {
|
||||
opts?.callback?.(null);
|
||||
setShown(false);
|
||||
opts.current?.callback?.(submitValue.current);
|
||||
submitValue.current = null;
|
||||
opts.current = undefined;
|
||||
}}
|
||||
footer={<Button text={t("prompt.ok")} keyboardShortcut="Enter" primary />}
|
||||
show={shown}
|
||||
stackable
|
||||
>
|
||||
<FormGroup name="prompt-dialog-answer" label={opts.current?.message} labelRef={labelRef}>
|
||||
<FormGroup label={opts?.message} labelRef={labelRef}>
|
||||
<FormTextBox
|
||||
name="prompt-dialog-answer"
|
||||
inputRef={answerRef}
|
||||
currentValue={value} onChange={setValue}
|
||||
readOnly={opts.current?.readOnly}
|
||||
/>
|
||||
currentValue={value} onChange={setValue} />
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -57,8 +57,7 @@ function RecentChangesDialogComponent() {
|
||||
header={
|
||||
<Button
|
||||
text={t("recent_changes.erase_notes_button")}
|
||||
size="small"
|
||||
style={{ padding: "0 10px" }}
|
||||
small style={{ padding: "0 10px" }}
|
||||
onClick={() => {
|
||||
server.post("notes/erase-deleted-notes-now").then(() => {
|
||||
setNeedsRefresh(true);
|
||||
|
||||
@@ -55,7 +55,7 @@ function RevisionsDialogComponent() {
|
||||
helpPageId="vZWERwf8U3nx"
|
||||
bodyStyle={{ display: "flex", height: "80vh" }}
|
||||
header={
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} size="small" style={{ padding: "0 10px" }}
|
||||
(!!revisions?.length && <Button text={t("revisions.delete_all_revisions")} small style={{ padding: "0 10px" }}
|
||||
onClick={async () => {
|
||||
const text = t("revisions.confirm_delete_all");
|
||||
|
||||
|
||||
@@ -83,8 +83,11 @@ function SortChildNotesDialogComponent() {
|
||||
label={t("sort_child_notes.sort_with_respect_to_different_character_sorting")}
|
||||
currentValue={sortNatural} onChange={setSortNatural}
|
||||
/>
|
||||
<FormGroup name="sort-locale" className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
|
||||
<FormTextBox currentValue={sortLocale} onChange={setSortLocale} />
|
||||
<FormGroup className="form-check" label={t("sort_child_notes.natural_sort_language")} description={t("sort_child_notes.the_language_code_for_natural_sort")}>
|
||||
<FormTextBox
|
||||
name="sort-locale"
|
||||
currentValue={sortLocale} onChange={setSortLocale}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -51,12 +51,13 @@ function UploadAttachmentsDialogComponent() {
|
||||
onHidden={() => setShown(false)}
|
||||
show={shown}
|
||||
>
|
||||
<FormGroup name="files" label={t("upload_attachments.choose_files")} description={description}>
|
||||
<FormGroup label={t("upload_attachments.choose_files")} description={description}>
|
||||
<FormFileUpload onChange={setFiles} multiple />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="shrink-images" label={t("upload_attachments.options")}>
|
||||
<FormGroup label={t("upload_attachments.options")}>
|
||||
<FormCheckbox
|
||||
name="shrink-images"
|
||||
hint={t("upload_attachments.tooltip")} label={t("upload_attachments.shrink_images")}
|
||||
currentValue={shrinkImages} onChange={setShrinkImages}
|
||||
/>
|
||||
|
||||
@@ -192,7 +192,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
* sets full height of container that contains note content for a subset of note-types
|
||||
*/
|
||||
checkFullHeight() {
|
||||
// https://github.com/TriliumNext/Trilium/issues/2522
|
||||
// https://github.com/zadam/trilium/issues/2522
|
||||
const isBackendNote = this.noteContext?.noteId === "_backendLog";
|
||||
const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
|
||||
const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "mermaid", "file"].includes(this.type ?? "");
|
||||
|
||||
@@ -81,7 +81,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
);
|
||||
|
||||
// there seems to be a race condition on Firefox which triggers the observer only before the widget is visible
|
||||
// (intersection is false). https://github.com/TriliumNext/Trilium/issues/4165
|
||||
// (intersection is false). https://github.com/zadam/trilium/issues/4165
|
||||
setTimeout(() => observer.observe(this.$widget[0]), 10);
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
setupNoteTitleTooltip() {
|
||||
// the following will dynamically set tree item's tooltip if the whole item's text is not currently visible
|
||||
// if the whole text is visible then no tooltip is show since that's unnecessarily distracting
|
||||
// see https://github.com/TriliumNext/Trilium/pull/1120 for discussion
|
||||
// see https://github.com/zadam/trilium/pull/1120 for discussion
|
||||
|
||||
// code inspired by https://gist.github.com/jtsternberg/c272d7de5b967cec2d3d
|
||||
const isEnclosing = ($container: JQuery<HTMLElement>, $sub: JQuery<HTMLElement>) => {
|
||||
@@ -952,7 +952,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
|
||||
await this.filterHoistedBranch(true);
|
||||
|
||||
// don't activate the active note, see discussion in https://github.com/TriliumNext/Trilium/issues/3664
|
||||
// don't activate the active note, see discussion in https://github.com/zadam/trilium/issues/3664
|
||||
}
|
||||
|
||||
async expandTree(node: Fancytree.FancytreeNode | null = null) {
|
||||
@@ -1181,7 +1181,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
/*
|
||||
* We're collapsing notes after a period of inactivity to "cleanup" the tree - users rarely
|
||||
* collapse the notes and the tree becomes unusuably large.
|
||||
* Some context: https://github.com/TriliumNext/Trilium/issues/1192
|
||||
* Some context: https://github.com/zadam/trilium/issues/1192
|
||||
*/
|
||||
|
||||
const noteIdsToKeepExpanded = new Set(
|
||||
@@ -1429,7 +1429,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
if (activeNodeFocused) {
|
||||
// needed by Firefox: https://github.com/TriliumNext/Trilium/issues/1865
|
||||
// needed by Firefox: https://github.com/zadam/trilium/issues/1865
|
||||
this.tree.$container.focus();
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,6 @@ interface QuickSearchResponse {
|
||||
highlightedNotePathTitle: string;
|
||||
contentSnippet?: string;
|
||||
highlightedContentSnippet?: string;
|
||||
attributeSnippet?: string;
|
||||
highlightedAttributeSnippet?: string;
|
||||
icon: string;
|
||||
}>;
|
||||
error: string;
|
||||
@@ -243,12 +241,7 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
<span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span>
|
||||
</div>`;
|
||||
|
||||
// Add attribute snippet (tags/attributes) below the title if available
|
||||
if (result.highlightedAttributeSnippet) {
|
||||
itemHtml += `<div style="font-size: 0.75em; color: var(--muted-text-color); opacity: 0.5; margin-left: 20px; margin-top: 2px; line-height: 1.2;" class="search-result-attributes">${result.highlightedAttributeSnippet}</div>`;
|
||||
}
|
||||
|
||||
// Add content snippet below the attributes if available
|
||||
// Add content snippet below the title if available
|
||||
if (result.highlightedContentSnippet) {
|
||||
itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`;
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface AdmonitionProps {
|
||||
type: "warning" | "note" | "caution";
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function Admonition({ type, children }: AdmonitionProps) {
|
||||
return (
|
||||
<div className={`admonition ${type}`} role="alert">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface AlertProps {
|
||||
type: "info" | "danger" | "warning";
|
||||
type: "info" | "danger";
|
||||
title?: string;
|
||||
children: ComponentChildren;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useRef, useMemo } from "preact/hooks";
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
interface ButtonProps {
|
||||
name?: string;
|
||||
/** Reference to the button element. Mostly useful for requesting focus. */
|
||||
buttonRef?: RefObject<HTMLButtonElement>;
|
||||
text: string;
|
||||
@@ -15,11 +14,11 @@ interface ButtonProps {
|
||||
onClick?: () => void;
|
||||
primary?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: "normal" | "small" | "micro";
|
||||
small?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, size, style }: ButtonProps) => {
|
||||
const Button = memo(({ buttonRef: _buttonRef, className, text, onClick, keyboardShortcut, icon, primary, disabled, small, style }: ButtonProps) => {
|
||||
// Memoize classes array to prevent recreation
|
||||
const classes = useMemo(() => {
|
||||
const classList: string[] = ["btn"];
|
||||
@@ -31,13 +30,11 @@ const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, ke
|
||||
if (className) {
|
||||
classList.push(className);
|
||||
}
|
||||
if (size === "small") {
|
||||
if (small) {
|
||||
classList.push("btn-sm");
|
||||
} else if (size === "micro") {
|
||||
classList.push("btn-micro");
|
||||
}
|
||||
return classList.join(" ");
|
||||
}, [primary, className, size]);
|
||||
}, [primary, className, small]);
|
||||
|
||||
const buttonRef = _buttonRef ?? useRef<HTMLButtonElement>(null);
|
||||
|
||||
@@ -55,7 +52,6 @@ const Button = memo(({ name, buttonRef: _buttonRef, className, text, onClick, ke
|
||||
|
||||
return (
|
||||
<button
|
||||
name={name}
|
||||
className={classes}
|
||||
type={onClick ? "button" : "submit"}
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
|
||||
interface ColumnProps {
|
||||
md?: number;
|
||||
children: ComponentChildren;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function Column({ md, children, className, style }: ColumnProps) {
|
||||
return (
|
||||
<div className={`col-md-${md ?? 6} ${className ?? ""}`} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,10 @@ import { Tooltip } from "bootstrap";
|
||||
import { useEffect, useRef, useMemo, useCallback } from "preact/hooks";
|
||||
import { escapeQuotes } from "../../services/utils";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { CSSProperties, memo } from "preact/compat";
|
||||
import { useUniqueName } from "./hooks";
|
||||
import { memo } from "preact/compat";
|
||||
|
||||
interface FormCheckboxProps {
|
||||
id?: string;
|
||||
name?: string;
|
||||
name: string;
|
||||
label: string | ComponentChildren;
|
||||
/**
|
||||
* If set, the checkbox label will be underlined and dotted, indicating a hint. When hovered, it will show the hint text.
|
||||
@@ -16,11 +14,9 @@ interface FormCheckboxProps {
|
||||
currentValue: boolean;
|
||||
disabled?: boolean;
|
||||
onChange(newValue: boolean): void;
|
||||
containerStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => {
|
||||
const id = _id ?? useUniqueName(name);
|
||||
const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint }: FormCheckboxProps) => {
|
||||
const labelRef = useRef<HTMLLabelElement>(null);
|
||||
|
||||
// Fix: Move useEffect outside conditional
|
||||
@@ -50,7 +46,7 @@ const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onCha
|
||||
const titleText = useMemo(() => hint ? escapeQuotes(hint) : undefined, [hint]);
|
||||
|
||||
return (
|
||||
<div className="form-checkbox" style={containerStyle}>
|
||||
<div className="form-checkbox">
|
||||
<label
|
||||
className="form-check-label tn-checkbox"
|
||||
style={labelStyle}
|
||||
@@ -58,10 +54,9 @@ const FormCheckbox = memo(({ name, id: _id, disabled, label, currentValue, onCha
|
||||
ref={labelRef}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
name={id}
|
||||
name={name}
|
||||
checked={currentValue || false}
|
||||
value="1"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,43 +1,24 @@
|
||||
import { cloneElement, ComponentChildren, RefObject, VNode } from "preact";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import { useUniqueName } from "./hooks";
|
||||
import { ComponentChildren, RefObject } from "preact";
|
||||
|
||||
interface FormGroupProps {
|
||||
name: string;
|
||||
labelRef?: RefObject<HTMLLabelElement>;
|
||||
label?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
children: VNode<any>;
|
||||
children: ComponentChildren;
|
||||
description?: string | ComponentChildren;
|
||||
disabled?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export default function FormGroup({ name, label, title, className, children, description, labelRef, disabled, style }: FormGroupProps) {
|
||||
const id = useUniqueName(name);
|
||||
const childWithId = cloneElement(children, { id });
|
||||
|
||||
export default function FormGroup({ label, title, className, children, description, labelRef }: FormGroupProps) {
|
||||
return (
|
||||
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title} style={style}>
|
||||
{ label &&
|
||||
<label style={{ width: "100%" }} ref={labelRef} htmlFor={id}>{label}</label>}
|
||||
|
||||
{childWithId}
|
||||
<div className={`form-group ${className}`} title={title}
|
||||
style={{ "margin-bottom": "15px" }}>
|
||||
<label style={{ width: "100%" }} ref={labelRef}>
|
||||
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
|
||||
{children}
|
||||
</label>
|
||||
|
||||
{description && <small className="form-text">{description}</small>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link FormGroup} but allows more than one child. Due to this behaviour, there is no automatic ID assignment.
|
||||
*/
|
||||
export function FormMultiGroup({ label, children }: { label: string, children: ComponentChildren }) {
|
||||
return (
|
||||
<div className={`form-group`}>
|
||||
{label && <label>{label}</label>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,30 @@
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { useUniqueName } from "./hooks";
|
||||
|
||||
interface FormRadioProps {
|
||||
name: string;
|
||||
currentValue?: string;
|
||||
values: {
|
||||
value: string;
|
||||
label: string | ComponentChildren;
|
||||
inlineDescription?: string | ComponentChildren;
|
||||
label: string;
|
||||
}[];
|
||||
onChange(newValue: string): void;
|
||||
}
|
||||
|
||||
export default function FormRadioGroup({ values, ...restProps }: FormRadioProps) {
|
||||
export default function FormRadioGroup({ name, values, currentValue, onChange }: FormRadioProps) {
|
||||
return (
|
||||
<div role="group">
|
||||
{(values || []).map(({ value, label, inlineDescription }) => (
|
||||
<div className="form-checkbox">
|
||||
<FormRadio
|
||||
value={value}
|
||||
label={label} inlineDescription={inlineDescription}
|
||||
labelClassName="form-check-label"
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormInlineRadioGroup({ values, ...restProps }: FormRadioProps) {
|
||||
return (
|
||||
<div role="group">
|
||||
{values.map(({ value, label }) => (<FormRadio value={value} label={label} {...restProps} />))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FormRadio({ name, value, label, currentValue, onChange, labelClassName, inlineDescription }: Omit<FormRadioProps, "values"> & { value: string, label: ComponentChildren, inlineDescription?: ComponentChildren, labelClassName?: string }) {
|
||||
return (
|
||||
<label className={`tn-radio ${labelClassName ?? ""}`}>
|
||||
<>
|
||||
{(values || []).map(({ value, label }) => (
|
||||
<div className="form-check">
|
||||
<label className="form-check-label tn-radio">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
name={useUniqueName(name)}
|
||||
name={name}
|
||||
value={value}
|
||||
checked={value === currentValue}
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
{inlineDescription ?
|
||||
<><strong>{label}</strong> - {inlineDescription}</>
|
||||
: label}
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)} />
|
||||
{label}
|
||||
</label>
|
||||
)
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import type { ComponentChildren } from "preact";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
|
||||
type OnChangeListener = (newValue: string) => void;
|
||||
|
||||
export interface FormSelectGroup<T> {
|
||||
title: string;
|
||||
items: T[];
|
||||
}
|
||||
|
||||
interface ValueConfig<T, Q> {
|
||||
values: Q[];
|
||||
/** The property of an item of {@link values} to be used as the key, uniquely identifying it. The key will be passed to the change listener. */
|
||||
keyProperty: keyof T;
|
||||
/** The property of an item of {@link values} to be used as the label, representing a human-readable version of the key. If missing, {@link keyProperty} will be used instead. */
|
||||
titleProperty?: keyof T;
|
||||
/** The current value of the combobox. The value will be looked up by going through {@link values} and looking an item whose {@link #keyProperty} value matches this one */
|
||||
currentValue?: string;
|
||||
}
|
||||
|
||||
interface FormSelectProps<T, Q> extends ValueConfig<T, Q> {
|
||||
id?: string;
|
||||
onChange: OnChangeListener;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combobox component that takes in any object array as data. Each item of the array is rendered as an item, and the key and values are obtained by looking into the object by a specified key.
|
||||
*/
|
||||
export default function FormSelect<T>({ id, onChange, style, ...restProps }: FormSelectProps<T, T>) {
|
||||
return (
|
||||
<FormSelectBody id={id} onChange={onChange} style={style}>
|
||||
<FormSelectGroup {...restProps} />
|
||||
</FormSelectBody>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link FormSelect}, but the top-level elements are actually groups.
|
||||
*/
|
||||
export function FormSelectWithGroups<T>({ id, values, keyProperty, titleProperty, currentValue, onChange }: FormSelectProps<T, FormSelectGroup<T>>) {
|
||||
return (
|
||||
<FormSelectBody id={id} onChange={onChange}>
|
||||
{values.map(({ title, items }) => {
|
||||
return (
|
||||
<optgroup label={title}>
|
||||
<FormSelectGroup values={items} keyProperty={keyProperty} titleProperty={titleProperty} currentValue={currentValue} />
|
||||
</optgroup>
|
||||
);
|
||||
})}
|
||||
</FormSelectBody>
|
||||
)
|
||||
}
|
||||
|
||||
function FormSelectBody({ id, children, onChange, style }: { id?: string, children: ComponentChildren, onChange: OnChangeListener, style?: CSSProperties }) {
|
||||
return (
|
||||
<select
|
||||
id={id}
|
||||
class="form-select"
|
||||
onChange={e => onChange((e.target as HTMLInputElement).value)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
function FormSelectGroup<T>({ values, keyProperty, titleProperty, currentValue }: ValueConfig<T, T>) {
|
||||
return values.map(item => {
|
||||
return (
|
||||
<option
|
||||
value={item[keyProperty] as any}
|
||||
selected={item[keyProperty] === currentValue}
|
||||
>
|
||||
{item[titleProperty ?? keyProperty] ?? item[keyProperty] as any}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
export default function FormText({ children }: { children: ComponentChildren }) {
|
||||
return <p className="form-text use-tn-links">{children}</p>
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
interface FormTextAreaProps {
|
||||
id?: string;
|
||||
currentValue: string;
|
||||
onBlur?(newValue: string): void;
|
||||
rows: number;
|
||||
}
|
||||
export default function FormTextArea({ id, onBlur, rows, currentValue }: FormTextAreaProps) {
|
||||
return (
|
||||
<textarea
|
||||
id={id}
|
||||
rows={rows}
|
||||
onBlur={(e) => {
|
||||
onBlur?.(e.currentTarget.value);
|
||||
}}
|
||||
style={{ width: "100%" }}
|
||||
>{currentValue}</textarea>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +1,27 @@
|
||||
import type { InputHTMLAttributes, RefObject } from "preact/compat";
|
||||
|
||||
interface FormTextBoxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onChange" | "onBlur" | "value"> {
|
||||
interface FormTextBoxProps extends Pick<InputHTMLAttributes<HTMLInputElement>, "placeholder" | "autoComplete" | "className" | "type" | "name" | "pattern" | "title" | "style"> {
|
||||
id?: string;
|
||||
currentValue?: string;
|
||||
onChange?(newValue: string, validity: ValidityState): void;
|
||||
onBlur?(newValue: string): void;
|
||||
onChange?(newValue: string): void;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export default function FormTextBox({ inputRef, className, type, currentValue, onChange, onBlur,...rest}: FormTextBoxProps) {
|
||||
if (type === "number" && currentValue) {
|
||||
const { min, max } = rest;
|
||||
const currentValueNum = parseInt(currentValue, 10);
|
||||
if (min && currentValueNum < parseInt(String(min), 10)) {
|
||||
currentValue = String(min);
|
||||
} else if (max && currentValueNum > parseInt(String(max), 10)) {
|
||||
currentValue = String(max);
|
||||
}
|
||||
}
|
||||
|
||||
export default function FormTextBox({ id, type, name, className, currentValue, onChange, autoComplete, inputRef, placeholder, title, pattern, style }: FormTextBoxProps) {
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={`form-control ${className ?? ""}`}
|
||||
type={type ?? "text"}
|
||||
className={`form-control ${className ?? ""}`}
|
||||
id={id}
|
||||
name={name}
|
||||
value={currentValue}
|
||||
onInput={onChange && (e => {
|
||||
const target = e.currentTarget;
|
||||
onChange?.(target.value, target.validity);
|
||||
})}
|
||||
onBlur={onBlur && (e => {
|
||||
const target = e.currentTarget;
|
||||
onBlur(target.value);
|
||||
})}
|
||||
{...rest}
|
||||
autoComplete={autoComplete}
|
||||
placeholder={placeholder}
|
||||
title={title}
|
||||
pattern={pattern}
|
||||
onInput={e => onChange?.(e.currentTarget.value)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormTextBoxWithUnit(props: FormTextBoxProps & { unit: string }) {
|
||||
return (
|
||||
<label class="input-group tn-number-unit-pair">
|
||||
<FormTextBox {...props} />
|
||||
<span class="input-group-text">{props.unit}</span>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
|
||||
interface KeyboardShortcutProps {
|
||||
actionName: KeyboardActionNames;
|
||||
}
|
||||
|
||||
export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) {
|
||||
|
||||
const [ action, setAction ] = useState<ActionKeyboardShortcut>();
|
||||
useEffect(() => {
|
||||
keyboard_actions.getAction(actionName).then(setAction);
|
||||
}, []);
|
||||
|
||||
if (!action) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{action.effectiveShortcuts?.map((shortcut, i) => {
|
||||
const keys = shortcut.split("+");
|
||||
return keys
|
||||
.map((key, i) => (
|
||||
<>
|
||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
||||
</>
|
||||
))
|
||||
}).reduce<any>((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), [])}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { ComponentChild } from "preact";
|
||||
|
||||
interface LinkButtonProps {
|
||||
onClick: () => void;
|
||||
text: ComponentChild;
|
||||
}
|
||||
|
||||
export default function LinkButton({ onClick, text }: LinkButtonProps) {
|
||||
return (
|
||||
<a class="tn-link" href="javascript:" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
}}>
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import type { RefObject } from "preact";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
|
||||
interface NoteAutocompleteProps {
|
||||
id?: string;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
text?: string;
|
||||
placeholder?: string;
|
||||
@@ -19,7 +18,7 @@ interface NoteAutocompleteProps {
|
||||
noteId?: string;
|
||||
}
|
||||
|
||||
export default function NoteAutocomplete({ id, inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
|
||||
export default function NoteAutocomplete({ inputRef: _ref, text, placeholder, onChange, onTextChange, container, containerStyle, opts, noteId, noteIdChanged }: NoteAutocompleteProps) {
|
||||
const ref = _ref ?? useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -75,7 +74,6 @@ export default function NoteAutocomplete({ id, inputRef: _ref, text, placeholder
|
||||
return (
|
||||
<div className="input-group" style={containerStyle}>
|
||||
<input
|
||||
id={id}
|
||||
ref={ref}
|
||||
className="note-autocomplete form-control"
|
||||
placeholder={placeholder ?? t("add_link.search_note")} />
|
||||
|
||||
@@ -24,7 +24,7 @@ function getProps({ className, html, style }: RawHtmlProps) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
||||
function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
||||
if (typeof html === "object" && "length" in html) {
|
||||
html = html[0];
|
||||
}
|
||||
|
||||
@@ -22,18 +22,11 @@ export default abstract class ReactBasicWidget extends BasicWidget {
|
||||
* @returns the rendered wrapped DOM element.
|
||||
*/
|
||||
export function renderReactWidget(parentComponent: Component, el: JSX.Element) {
|
||||
return renderReactWidgetAtElement(parentComponent, el, new DocumentFragment()).children();
|
||||
}
|
||||
|
||||
export function renderReactWidgetAtElement(parentComponent: Component, el: JSX.Element, container: Element | DocumentFragment) {
|
||||
const renderContainer = new DocumentFragment();
|
||||
render((
|
||||
<ParentComponent.Provider value={parentComponent}>
|
||||
{el}
|
||||
</ParentComponent.Provider>
|
||||
), container);
|
||||
return $(container) as JQuery<HTMLElement>;
|
||||
}
|
||||
|
||||
export function disposeReactWidget(container: Element) {
|
||||
render(null, container);
|
||||
), renderContainer);
|
||||
return $(renderContainer.firstChild as HTMLElement);
|
||||
}
|
||||
@@ -1,15 +1,7 @@
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||
import { EventData, EventNames } from "../../components/app_context";
|
||||
import { ParentComponent } from "./ReactBasicWidget";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import { OptionNames } from "@triliumnext/commons";
|
||||
import options, { type OptionValue } from "../../services/options";
|
||||
import utils, { reloadFrontendApp } from "../../services/utils";
|
||||
import Component from "../../components/component";
|
||||
import server from "../../services/server";
|
||||
|
||||
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
|
||||
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
|
||||
|
||||
/**
|
||||
* Allows a React component to react to Trilium events (e.g. `entitiesReloaded`). When the desired event is triggered, the handler is invoked with the event parameters.
|
||||
@@ -20,67 +12,32 @@ const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any
|
||||
* @param handler the handler to be invoked when the event is triggered.
|
||||
* @param enabled determines whether the event should be listened to or not. Useful to conditionally limit the listener based on a state (e.g. a modal being displayed).
|
||||
*/
|
||||
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: TriliumEventHandler<T>, enabled = true) {
|
||||
export default function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void, enabled = true) {
|
||||
const parentWidget = useContext(ParentComponent);
|
||||
if (!parentWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlerName = `${eventName}Event`;
|
||||
const customHandler = useMemo(() => {
|
||||
return async (data: EventData<T>) => {
|
||||
// Inform the attached event listeners.
|
||||
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName) ?? [];
|
||||
for (const eventHandler of eventHandlers) {
|
||||
eventHandler(data);
|
||||
}
|
||||
}
|
||||
}, [ eventName, parentWidget ]);
|
||||
|
||||
useEffect(() => {
|
||||
// Attach to the list of handlers.
|
||||
let handlersByWidget = registeredHandlers.get(parentWidget);
|
||||
if (!handlersByWidget) {
|
||||
handlersByWidget = new Map();
|
||||
registeredHandlers.set(parentWidget, handlersByWidget);
|
||||
}
|
||||
|
||||
let handlersByWidgetAndEventName = handlersByWidget.get(eventName);
|
||||
if (!handlersByWidgetAndEventName) {
|
||||
handlersByWidgetAndEventName = [];
|
||||
handlersByWidget.set(eventName, handlersByWidgetAndEventName);
|
||||
}
|
||||
|
||||
if (!handlersByWidgetAndEventName.includes(handler)) {
|
||||
handlersByWidgetAndEventName.push(handler);
|
||||
}
|
||||
|
||||
// Apply the custom event handler.
|
||||
if (parentWidget[handlerName] && parentWidget[handlerName] !== customHandler) {
|
||||
console.warn(`Widget ${parentWidget.componentId} already had an event listener and it was replaced by the React one.`);
|
||||
}
|
||||
|
||||
parentWidget[handlerName] = customHandler;
|
||||
|
||||
return () => {
|
||||
const eventHandlers = registeredHandlers.get(parentWidget)?.get(eventName);
|
||||
if (!eventHandlers || !eventHandlers.includes(handler)) {
|
||||
if (!parentWidget || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the event handler from the array.
|
||||
const newEventHandlers = eventHandlers.filter(e => e !== handler);
|
||||
if (newEventHandlers.length) {
|
||||
registeredHandlers.get(parentWidget)?.set(eventName, newEventHandlers);
|
||||
} else {
|
||||
registeredHandlers.get(parentWidget)?.delete(eventName);
|
||||
}
|
||||
// Create a unique handler name for this specific event listener
|
||||
const handlerName = `${eventName}Event`;
|
||||
const originalHandler = parentWidget[handlerName];
|
||||
|
||||
if (!registeredHandlers.get(parentWidget)?.size) {
|
||||
registeredHandlers.delete(parentWidget);
|
||||
// Override the event handler to call our handler
|
||||
parentWidget[handlerName] = async function(data: EventData<T>) {
|
||||
// Call original handler if it exists
|
||||
if (originalHandler) {
|
||||
await originalHandler.call(parentWidget, data);
|
||||
}
|
||||
// Call our React component's handler
|
||||
handler(data);
|
||||
};
|
||||
}, [ eventName, parentWidget, handler ]);
|
||||
|
||||
// Cleanup: restore original handler on unmount or when disabled
|
||||
return () => {
|
||||
parentWidget[handlerName] = originalHandler;
|
||||
};
|
||||
}, [parentWidget, enabled, eventName, handler]);
|
||||
}
|
||||
|
||||
export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) {
|
||||
@@ -107,115 +64,3 @@ export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000)
|
||||
|
||||
return spacedUpdateRef.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a React component to read and write a Trilium option, while also watching for external changes.
|
||||
*
|
||||
* Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if
|
||||
* the option is changed somewhere else in the client.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @param needsRefresh whether to reload the frontend whenever the value is changed.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: OptionValue) => Promise<void>] {
|
||||
const initialValue = options.get(name);
|
||||
const [ value, setValue ] = useState(initialValue);
|
||||
|
||||
const wrappedSetValue = useMemo(() => {
|
||||
return async (newValue: OptionValue) => {
|
||||
await options.save(name, newValue);
|
||||
|
||||
if (needsRefresh) {
|
||||
reloadFrontendApp(`option change: ${name}`);
|
||||
}
|
||||
}
|
||||
}, [ name, needsRefresh ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
|
||||
if (loadResults.getOptionNames().includes(name)) {
|
||||
const newValue = options.get(name);
|
||||
setValue(newValue);
|
||||
}
|
||||
}, [ name ]));
|
||||
|
||||
return [
|
||||
value,
|
||||
wrappedSetValue
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but the value is converted to and from a boolean instead of a string.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @param needsRefresh whether to reload the frontend whenever the value is changed.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] {
|
||||
const [ value, setValue ] = useTriliumOption(name, needsRefresh);
|
||||
return [
|
||||
(value === "true"),
|
||||
(newValue) => setValue(newValue ? "true" : "false")
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but the value is converted to and from a int instead of a string.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @param needsRefresh whether to reload the frontend whenever the value is changed.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOptionInt(name: OptionNames): [number, (newValue: number) => Promise<void>] {
|
||||
const [ value, setValue ] = useTriliumOption(name);
|
||||
return [
|
||||
(parseInt(value, 10)),
|
||||
(newValue) => setValue(newValue)
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string.
|
||||
*
|
||||
* @param name the name of the option to listen for.
|
||||
* @returns an array where the first value is the current option value and the second value is the setter.
|
||||
*/
|
||||
export function useTriliumOptionJson<T>(name: OptionNames): [ T, (newValue: T) => Promise<void> ] {
|
||||
const [ value, setValue ] = useTriliumOption(name);
|
||||
return [
|
||||
(JSON.parse(value) as T),
|
||||
(newValue => setValue(JSON.stringify(newValue)))
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to {@link useTriliumOption}, but operates with multiple options at once.
|
||||
*
|
||||
* @param names the name of the option to listen for.
|
||||
* @returns an array where the first value is a map where the keys are the option names and the values, and the second value is the setter which takes in the same type of map and saves them all at once.
|
||||
*/
|
||||
export function useTriliumOptions<T extends OptionNames>(...names: T[]) {
|
||||
const values: Record<string, string> = {};
|
||||
for (const name of names) {
|
||||
values[name] = options.get(name);
|
||||
}
|
||||
|
||||
return [
|
||||
values as Record<T, string>,
|
||||
options.saveMany
|
||||
] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique name via a random alphanumeric string of a fixed length.
|
||||
*
|
||||
* <p>
|
||||
* Generally used to assign names to inputs that are unique, especially useful for widgets inside tabs.
|
||||
*
|
||||
* @param prefix a prefix to add to the unique name.
|
||||
* @returns a name with the given prefix and a random alpanumeric string appended to it.
|
||||
*/
|
||||
export function useUniqueName(prefix?: string) {
|
||||
return useMemo(() => (prefix ? prefix + "-" : "") + utils.randomString(10), [ prefix ]);
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default class InheritedAttributesWidget extends NoteContextAwareWidget {
|
||||
if (a.noteId === b.noteId) {
|
||||
return a.position - b.position;
|
||||
} else {
|
||||
// inherited attributes should stay grouped: https://github.com/TriliumNext/Trilium/issues/3761
|
||||
// inherited attributes should stay grouped: https://github.com/zadam/trilium/issues/3761
|
||||
return a.noteId < b.noteId ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ const TPL = /*html*/`
|
||||
<div class="promoted-attributes-widget">
|
||||
<style>
|
||||
body.mobile .promoted-attributes-widget {
|
||||
/* https://github.com/TriliumNext/Trilium/issues/4468 */
|
||||
/* https://github.com/zadam/trilium/issues/4468 */
|
||||
flex-shrink: 0.4;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default class SharedInfoWidget extends NoteContextAwareWidget {
|
||||
let host = location.host;
|
||||
if (host.endsWith("/")) {
|
||||
// seems like IE has trailing slash
|
||||
// https://github.com/TriliumNext/Trilium/issues/3782
|
||||
// https://github.com/zadam/trilium/issues/3782
|
||||
host = host.substr(0, host.length - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +360,7 @@ export default class TocWidget extends RightPanelWidget {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce indent if a larger headings are not being used: https://github.com/TriliumNext/Trilium/issues/4363
|
||||
* Reduce indent if a larger headings are not being used: https://github.com/zadam/trilium/issues/4363
|
||||
*/
|
||||
pullLeft($toc: JQuery<HTMLElement>) {
|
||||
while (true) {
|
||||
@@ -390,7 +390,7 @@ export default class TocWidget extends RightPanelWidget {
|
||||
// temporarily" (ie "edit this note" button) without any
|
||||
// intervening events, do the readonly calculation at navigation
|
||||
// time and not at outline creation time
|
||||
// See https://github.com/TriliumNext/Trilium/issues/2828
|
||||
// See https://github.com/zadam/trilium/issues/2828
|
||||
const isDocNote = this.note.type === "doc";
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const TPL = /*html*/`
|
||||
}
|
||||
|
||||
/* Conflict between excalidraw and bootstrap classes keeps the menu hidden */
|
||||
/* https://github.com/TriliumNext/Trilium/issues/3780 */
|
||||
/* https://github.com/zadam/trilium/issues/3780 */
|
||||
/* https://github.com/excalidraw/excalidraw/issues/6567 */
|
||||
.excalidraw .dropdown-menu {
|
||||
display: block;
|
||||
|
||||
199
apps/client/src/widgets/type_widgets/content_widget.ts
Normal file
199
apps/client/src/widgets/type_widgets/content_widget.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import ElectronIntegrationOptions from "./options/appearance/electron_integration.js";
|
||||
import ThemeOptions from "./options/appearance/theme.js";
|
||||
import FontsOptions from "./options/appearance/fonts.js";
|
||||
import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
|
||||
import KeyboardShortcutsOptions from "./options/shortcuts.js";
|
||||
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
|
||||
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
|
||||
import HighlightsListOptions from "./options/text_notes/highlights_list.js";
|
||||
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
|
||||
import DateTimeFormatOptions from "./options/text_notes/date_time_format.js";
|
||||
import CodeEditorOptions from "./options/code_notes/code_editor.js";
|
||||
import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_only_size.js";
|
||||
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
|
||||
import ImageOptions from "./options/images/images.js";
|
||||
import SpellcheckOptions from "./options/spellcheck.js";
|
||||
import PasswordOptions from "./options/password/password.js";
|
||||
import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js";
|
||||
import EtapiOptions from "./options/etapi.js";
|
||||
import BackupOptions from "./options/backup.js";
|
||||
import SyncOptions from "./options/sync.js";
|
||||
import SearchEngineOptions from "./options/other/search_engine.js";
|
||||
import TrayOptions from "./options/other/tray.js";
|
||||
import NoteErasureTimeoutOptions from "./options/other/note_erasure_timeout.js";
|
||||
import RevisionsSnapshotIntervalOptions from "./options/other/revisions_snapshot_interval.js";
|
||||
import RevisionSnapshotsLimitOptions from "./options/other/revision_snapshots_limit.js";
|
||||
import NetworkConnectionsOptions from "./options/other/network_connections.js";
|
||||
import HtmlImportTagsOptions from "./options/other/html_import_tags.js";
|
||||
import AdvancedSyncOptions from "./options/advanced/sync.js";
|
||||
import DatabaseIntegrityCheckOptions from "./options/advanced/database_integrity_check.js";
|
||||
import VacuumDatabaseOptions from "./options/advanced/vacuum_database.js";
|
||||
import DatabaseAnonymizationOptions from "./options/advanced/database_anonymization.js";
|
||||
import BackendLogWidget from "./content/backend_log.js";
|
||||
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
||||
import RibbonOptions from "./options/appearance/ribbon.js";
|
||||
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
|
||||
import LocalizationOptions from "./options/i18n/i18n.js";
|
||||
import CodeBlockOptions from "./options/text_notes/code_block.js";
|
||||
import EditorOptions from "./options/text_notes/editor.js";
|
||||
import ShareSettingsOptions from "./options/other/share_settings.js";
|
||||
import AiSettingsOptions from "./options/ai_settings.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import LanguageOptions from "./options/i18n/language.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import CodeTheme from "./options/code_notes/code_theme.js";
|
||||
import RelatedSettings from "./options/appearance/related_settings.js";
|
||||
import EditorFeaturesOptions from "./options/text_notes/features.js";
|
||||
|
||||
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||
<style>
|
||||
.type-contentWidget .note-detail {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content {
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail.full-height .note-detail-content-widget-content {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-detail-content-widget-content"></div>
|
||||
</div>`;
|
||||
|
||||
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
|
||||
|
||||
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (typeof NoteContextAwareWidget)[]> = {
|
||||
_optionsAppearance: [
|
||||
ThemeOptions,
|
||||
FontsOptions,
|
||||
ElectronIntegrationOptions,
|
||||
MaxContentWidthOptions,
|
||||
RibbonOptions
|
||||
],
|
||||
_optionsShortcuts: [
|
||||
KeyboardShortcutsOptions
|
||||
],
|
||||
_optionsTextNotes: [
|
||||
EditorOptions,
|
||||
EditorFeaturesOptions,
|
||||
HeadingStyleOptions,
|
||||
CodeBlockOptions,
|
||||
TableOfContentsOptions,
|
||||
HighlightsListOptions,
|
||||
TextAutoReadOnlySizeOptions,
|
||||
DateTimeFormatOptions
|
||||
],
|
||||
_optionsCodeNotes: [
|
||||
CodeEditorOptions,
|
||||
CodeTheme,
|
||||
CodeMimeTypesOptions,
|
||||
CodeAutoReadOnlySizeOptions
|
||||
],
|
||||
_optionsImages: [
|
||||
ImageOptions
|
||||
],
|
||||
_optionsSpellcheck: [
|
||||
SpellcheckOptions
|
||||
],
|
||||
_optionsPassword: [
|
||||
PasswordOptions,
|
||||
ProtectedSessionTimeoutOptions
|
||||
],
|
||||
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||
_optionsEtapi: [
|
||||
EtapiOptions
|
||||
],
|
||||
_optionsBackup: [
|
||||
BackupOptions
|
||||
],
|
||||
_optionsSync: [
|
||||
SyncOptions
|
||||
],
|
||||
_optionsAi: [AiSettingsOptions],
|
||||
_optionsOther: [
|
||||
SearchEngineOptions,
|
||||
TrayOptions,
|
||||
NoteErasureTimeoutOptions,
|
||||
AttachmentErasureTimeoutOptions,
|
||||
RevisionsSnapshotIntervalOptions,
|
||||
RevisionSnapshotsLimitOptions,
|
||||
HtmlImportTagsOptions,
|
||||
ShareSettingsOptions,
|
||||
NetworkConnectionsOptions
|
||||
],
|
||||
_optionsLocalization: [
|
||||
LocalizationOptions,
|
||||
LanguageOptions
|
||||
],
|
||||
_optionsAdvanced: [
|
||||
AdvancedSyncOptions,
|
||||
DatabaseIntegrityCheckOptions,
|
||||
DatabaseAnonymizationOptions,
|
||||
VacuumDatabaseOptions
|
||||
],
|
||||
_backendLog: [
|
||||
BackendLogWidget
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Type widget that displays one or more widgets based on the type of note, generally used for options and other interactive notes such as the backend log.
|
||||
*
|
||||
* One important aspect is that, like its parent {@link TypeWidget}, the content widgets don't receive all events by default and they must be manually added
|
||||
* to the propagation list in {@link TypeWidget.handleEventInChildren}.
|
||||
*/
|
||||
export default class ContentWidgetTypeWidget extends TypeWidget {
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private widget?: BasicWidget;
|
||||
|
||||
static getType() {
|
||||
return "contentWidget";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find(".note-detail-content-widget-content");
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$content.empty();
|
||||
this.children = [];
|
||||
|
||||
const contentWidgets = [
|
||||
...((CONTENT_WIDGETS as Record<string, typeof NoteContextAwareWidget[]>)[note.noteId]),
|
||||
RelatedSettings
|
||||
];
|
||||
this.$content.toggleClass("options", note.noteId.startsWith("_options"));
|
||||
|
||||
if (contentWidgets) {
|
||||
for (const clazz of contentWidgets) {
|
||||
const widget = new clazz();
|
||||
|
||||
if (this.noteContext) {
|
||||
await widget.handleEvent("setNoteContext", { noteContext: this.noteContext });
|
||||
}
|
||||
this.child(widget);
|
||||
|
||||
this.$content.append(widget.render());
|
||||
this.widget = widget;
|
||||
await widget.refresh();
|
||||
}
|
||||
} else {
|
||||
this.$content.append(t("content_widget.unknown_widget", { id: note.noteId }));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type BasicWidget from "../basic_widget.js";
|
||||
import type { JSX } from "preact/jsx-runtime";
|
||||
import AppearanceSettings from "./options/appearance.jsx";
|
||||
import { disposeReactWidget, renderReactWidget, renderReactWidgetAtElement } from "../react/ReactBasicWidget.jsx";
|
||||
import ImageSettings from "./options/images.jsx";
|
||||
import AdvancedSettings from "./options/advanced.jsx";
|
||||
import InternationalizationOptions from "./options/i18n.jsx";
|
||||
import SyncOptions from "./options/sync.jsx";
|
||||
import EtapiSettings from "./options/etapi.js";
|
||||
import BackupSettings from "./options/backup.js";
|
||||
import SpellcheckSettings from "./options/spellcheck.js";
|
||||
import PasswordSettings from "./options/password.jsx";
|
||||
import ShortcutSettings from "./options/shortcuts.js";
|
||||
import TextNoteSettings from "./options/text_notes.jsx";
|
||||
import CodeNoteSettings from "./options/code_notes.jsx";
|
||||
import OtherSettings from "./options/other.jsx";
|
||||
import BackendLogWidget from "./content/backend_log.js";
|
||||
import MultiFactorAuthenticationSettings from "./options/multi_factor_authentication.js";
|
||||
import AiSettings from "./options/ai_settings.jsx";
|
||||
|
||||
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
|
||||
<style>
|
||||
.type-contentWidget .note-detail {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail-content-widget-content {
|
||||
padding: 15px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-detail.full-height .note-detail-content-widget-content {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-detail-content-widget-content"></div>
|
||||
</div>`;
|
||||
|
||||
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
|
||||
|
||||
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextAwareWidget)[] | JSX.Element)> = {
|
||||
_optionsAppearance: <AppearanceSettings />,
|
||||
_optionsShortcuts: <ShortcutSettings />,
|
||||
_optionsTextNotes: <TextNoteSettings />,
|
||||
_optionsCodeNotes: <CodeNoteSettings />,
|
||||
_optionsImages: <ImageSettings />,
|
||||
_optionsSpellcheck: <SpellcheckSettings />,
|
||||
_optionsPassword: <PasswordSettings />,
|
||||
_optionsMFA: <MultiFactorAuthenticationSettings />,
|
||||
_optionsEtapi: <EtapiSettings />,
|
||||
_optionsBackup: <BackupSettings />,
|
||||
_optionsSync: <SyncOptions />,
|
||||
_optionsAi: <AiSettings />,
|
||||
_optionsOther: <OtherSettings />,
|
||||
_optionsLocalization: <InternationalizationOptions />,
|
||||
_optionsAdvanced: <AdvancedSettings />,
|
||||
_backendLog: [
|
||||
BackendLogWidget
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Type widget that displays one or more widgets based on the type of note, generally used for options and other interactive notes such as the backend log.
|
||||
*
|
||||
* One important aspect is that, like its parent {@link TypeWidget}, the content widgets don't receive all events by default and they must be manually added
|
||||
* to the propagation list in {@link TypeWidget.handleEventInChildren}.
|
||||
*/
|
||||
export default class ContentWidgetTypeWidget extends TypeWidget {
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private widget?: BasicWidget;
|
||||
|
||||
static getType() {
|
||||
return "contentWidget";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find(".note-detail-content-widget-content");
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async doRefresh(note: FNote) {
|
||||
this.$content.empty();
|
||||
this.children = [];
|
||||
|
||||
const contentWidgets = (CONTENT_WIDGETS as Record<string, (typeof NoteContextAwareWidget[] | JSX.Element)>)[note.noteId];
|
||||
this.$content.toggleClass("options", note.noteId.startsWith("_options"));
|
||||
|
||||
// Unknown widget.
|
||||
if (!contentWidgets) {
|
||||
this.$content.append(t("content_widget.unknown_widget", { id: note.noteId }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy widget.
|
||||
if (Array.isArray(contentWidgets)) {
|
||||
for (const clazz of contentWidgets) {
|
||||
const widget = new clazz();
|
||||
|
||||
if (this.noteContext) {
|
||||
await widget.handleEvent("setNoteContext", { noteContext: this.noteContext });
|
||||
}
|
||||
this.child(widget);
|
||||
|
||||
this.$content.append(widget.render());
|
||||
this.widget = widget;
|
||||
await widget.refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// React widget.
|
||||
renderReactWidgetAtElement(this, contentWidgets, this.$content[0]);
|
||||
}
|
||||
|
||||
cleanup(): void {
|
||||
if (this.noteId) {
|
||||
const contentWidgets = (CONTENT_WIDGETS as Record<string, (typeof NoteContextAwareWidget[] | JSX.Element)>)[this.noteId];
|
||||
if (contentWidgets && !Array.isArray(contentWidgets)) {
|
||||
disposeReactWidget(this.$content[0]);
|
||||
}
|
||||
}
|
||||
|
||||
super.cleanup();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
import { AnonymizedDbResponse, DatabaseAnonymizeResponse, DatabaseCheckIntegrityResponse } from "@triliumnext/commons";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import OptionsSection from "./components/OptionsSection"
|
||||
import Column from "../../react/Column";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
export default function AdvancedSettings() {
|
||||
return <>
|
||||
<AdvancedSyncOptions />
|
||||
<DatabaseIntegrityOptions />
|
||||
<DatabaseAnonymizationOptions />
|
||||
<VacuumDatabaseOptions />
|
||||
</>;
|
||||
}
|
||||
|
||||
function AdvancedSyncOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("sync.title")}>
|
||||
<Button
|
||||
text={t("sync.force_full_sync_button")}
|
||||
onClick={async () => {
|
||||
await server.post("sync/force-full-sync");
|
||||
toast.showMessage(t("sync.full_sync_triggered"));
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={t("sync.fill_entity_changes_button")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("sync.filling_entity_changes"));
|
||||
await server.post("sync/fill-entity-changes");
|
||||
toast.showMessage(t("sync.sync_rows_filled_successfully"));
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseIntegrityOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("database_integrity_check.title")}>
|
||||
<FormText>{t("database_integrity_check.description")}</FormText>
|
||||
|
||||
<Button
|
||||
text={t("database_integrity_check.check_button")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("database_integrity_check.checking_integrity"));
|
||||
|
||||
const { results } = await server.get<DatabaseCheckIntegrityResponse>("database/check-integrity");
|
||||
|
||||
if (results.length === 1 && results[0].integrity_check === "ok") {
|
||||
toast.showMessage(t("database_integrity_check.integrity_check_succeeded"));
|
||||
} else {
|
||||
toast.showMessage(t("database_integrity_check.integrity_check_failed", { results: JSON.stringify(results, null, 2) }), 15000);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={t("consistency_checks.find_and_fix_button")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("consistency_checks.finding_and_fixing_message"));
|
||||
await server.post("database/find-and-fix-consistency-issues");
|
||||
toast.showMessage(t("consistency_checks.issues_fixed_message"));
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function DatabaseAnonymizationOptions() {
|
||||
const [ existingAnonymizedDatabases, setExistingAnonymizedDatabases ] = useState<AnonymizedDbResponse[]>([]);
|
||||
|
||||
function refreshAnonymizedDatabase() {
|
||||
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then(setExistingAnonymizedDatabases);
|
||||
}
|
||||
|
||||
useEffect(refreshAnonymizedDatabase, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("database_anonymization.title")}>
|
||||
<FormText>{t("database_anonymization.choose_anonymization")}</FormText>
|
||||
|
||||
<div className="row">
|
||||
<DatabaseAnonymizationOption
|
||||
title={t("database_anonymization.full_anonymization")}
|
||||
description={t("database_anonymization.full_anonymization_description")}
|
||||
buttonText={t("database_anonymization.save_fully_anonymized_database")}
|
||||
buttonClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/full");
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DatabaseAnonymizationOption
|
||||
title={t("database_anonymization.light_anonymization")}
|
||||
description={t("database_anonymization.light_anonymization_description")}
|
||||
buttonText={t("database_anonymization.save_lightly_anonymized_database")}
|
||||
buttonClick={async () => {
|
||||
toast.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
|
||||
const resp = await server.post<DatabaseAnonymizeResponse>("database/anonymize/light");
|
||||
|
||||
if (!resp.success) {
|
||||
toast.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toast.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
refreshAnonymizedDatabase();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<ExistingAnonymizedDatabases databases={existingAnonymizedDatabases} />
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function DatabaseAnonymizationOption({ title, description, buttonText, buttonClick }: { title: string, description: string, buttonText: string, buttonClick: () => void }) {
|
||||
return (
|
||||
<Column md={6} style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", marginTop: "1em" }}>
|
||||
<h5>{title}</h5>
|
||||
<FormText>{description}</FormText>
|
||||
<Button text={buttonText} onClick={buttonClick} />
|
||||
</Column>
|
||||
)
|
||||
}
|
||||
|
||||
function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbResponse[] }) {
|
||||
if (!databases.length) {
|
||||
return <FormText>{t("database_anonymization.no_anonymized_database_yet")}</FormText>
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table table-stripped">
|
||||
<thead>
|
||||
<th>{t("database_anonymization.existing_anonymized_databases")}</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{databases.map(({ filePath }) => (
|
||||
<tr>
|
||||
<td>{filePath}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
function VacuumDatabaseOptions() {
|
||||
return (
|
||||
<OptionsSection title={t("vacuum_database.title")}>
|
||||
<FormText>{t("vacuum_database.description")}</FormText>
|
||||
|
||||
<Button
|
||||
text={t("vacuum_database.button_text")}
|
||||
onClick={async () => {
|
||||
toast.showMessage(t("vacuum_database.vacuuming_database"));
|
||||
await server.post("database/vacuum-database");
|
||||
toast.showMessage(t("vacuum_database.database_vacuumed"));
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<style>
|
||||
.database-database-anonymization-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.database-database-anonymization-option p {
|
||||
margin-top: .75em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h4>${t("database_anonymization.title")}</h4>
|
||||
|
||||
<div class="row">
|
||||
<p class="form-text">${t("database_anonymization.choose_anonymization")}</p>
|
||||
|
||||
<div class="col-md-6 database-database-anonymization-option">
|
||||
<h5>${t("database_anonymization.full_anonymization")}</h5>
|
||||
|
||||
<p class="form-text">${t("database_anonymization.full_anonymization_description")}</p>
|
||||
<button class="anonymize-full-button btn btn-secondary">${t("database_anonymization.save_fully_anonymized_database")}</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 database-database-anonymization-option">
|
||||
<h5>${t("database_anonymization.light_anonymization")}</h5>
|
||||
|
||||
<p class="form-text">${t("database_anonymization.light_anonymization_description")}</p>
|
||||
|
||||
<button class="anonymize-light-button btn btn-secondary">${t("database_anonymization.save_lightly_anonymized_database")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<table class="existing-anonymized-databases-table table table-stripped">
|
||||
<thead>
|
||||
<th>${t("database_anonymization.existing_anonymized_databases")}</th>
|
||||
</thead>
|
||||
<tbody class="existing-anonymized-databases">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
|
||||
// TODO: Deduplicate with server
|
||||
interface AnonymizeResponse {
|
||||
success: boolean;
|
||||
anonymizedFilePath: string;
|
||||
}
|
||||
|
||||
interface AnonymizedDbResponse {
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export default class DatabaseAnonymizationOptions extends OptionsWidget {
|
||||
|
||||
private $anonymizeFullButton!: JQuery<HTMLElement>;
|
||||
private $anonymizeLightButton!: JQuery<HTMLElement>;
|
||||
private $existingAnonymizedDatabases!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$anonymizeFullButton = this.$widget.find(".anonymize-full-button");
|
||||
this.$anonymizeLightButton = this.$widget.find(".anonymize-light-button");
|
||||
this.$anonymizeFullButton.on("click", async () => {
|
||||
toastService.showMessage(t("database_anonymization.creating_fully_anonymized_database"));
|
||||
|
||||
const resp = await server.post<AnonymizeResponse>("database/anonymize/full");
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toastService.showMessage(t("database_anonymization.successfully_created_fully_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$anonymizeLightButton.on("click", async () => {
|
||||
toastService.showMessage(t("database_anonymization.creating_lightly_anonymized_database"));
|
||||
|
||||
const resp = await server.post<AnonymizeResponse>("database/anonymize/light");
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(t("database_anonymization.error_creating_anonymized_database"));
|
||||
} else {
|
||||
toastService.showMessage(t("database_anonymization.successfully_created_lightly_anonymized_database", { anonymizedFilePath: resp.anonymizedFilePath }), 10000);
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.$existingAnonymizedDatabases = this.$widget.find(".existing-anonymized-databases");
|
||||
}
|
||||
|
||||
optionsLoaded(options: OptionMap) {
|
||||
server.get<AnonymizedDbResponse[]>("database/anonymized-databases").then((anonymizedDatabases) => {
|
||||
this.$existingAnonymizedDatabases.empty();
|
||||
|
||||
if (!anonymizedDatabases.length) {
|
||||
anonymizedDatabases = [{ filePath: t("database_anonymization.no_anonymized_database_yet") }];
|
||||
}
|
||||
|
||||
for (const { filePath } of anonymizedDatabases) {
|
||||
this.$existingAnonymizedDatabases.append($("<tr>").append($("<td>").text(filePath)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("database_integrity_check.title")}</h4>
|
||||
|
||||
<p class="form-text">${t("database_integrity_check.description")}</p>
|
||||
|
||||
<button class="check-integrity-button btn btn-secondary">${t("database_integrity_check.check_button")}</button>
|
||||
<button class="find-and-fix-consistency-issues-button btn btn-secondary">${t("consistency_checks.find_and_fix_button")}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// TODO: Deduplicate with server
|
||||
interface Response {
|
||||
results: {
|
||||
integrity_check: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default class DatabaseIntegrityCheckOptions extends OptionsWidget {
|
||||
|
||||
private $checkIntegrityButton!: JQuery<HTMLElement>;
|
||||
private $findAndFixConsistencyIssuesButton!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$checkIntegrityButton = this.$widget.find(".check-integrity-button");
|
||||
this.$checkIntegrityButton.on("click", async () => {
|
||||
toastService.showMessage(t("database_integrity_check.checking_integrity"));
|
||||
|
||||
const { results } = await server.get<Response>("database/check-integrity");
|
||||
|
||||
if (results.length === 1 && results[0].integrity_check === "ok") {
|
||||
toastService.showMessage(t("database_integrity_check.integrity_check_succeeded"));
|
||||
} else {
|
||||
toastService.showMessage(t("database_integrity_check.integrity_check_failed", { results: JSON.stringify(results, null, 2) }), 15000);
|
||||
}
|
||||
});
|
||||
|
||||
this.$findAndFixConsistencyIssuesButton = this.$widget.find(".find-and-fix-consistency-issues-button");
|
||||
this.$findAndFixConsistencyIssuesButton.on("click", async () => {
|
||||
toastService.showMessage(t("consistency_checks.finding_and_fixing_message"));
|
||||
|
||||
await server.post("database/find-and-fix-consistency-issues");
|
||||
|
||||
toastService.showMessage(t("consistency_checks.issues_fixed_message"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("sync.title")}</h4>
|
||||
<button class="force-full-sync-button btn btn-secondary">${t("sync.force_full_sync_button")}</button>
|
||||
|
||||
<button class="fill-entity-changes-button btn btn-secondary">${t("sync.fill_entity_changes_button")}</button>
|
||||
</div>`;
|
||||
|
||||
export default class AdvancedSyncOptions extends OptionsWidget {
|
||||
|
||||
private $forceFullSyncButton!: JQuery<HTMLElement>;
|
||||
private $fillEntityChangesButton!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$forceFullSyncButton = this.$widget.find(".force-full-sync-button");
|
||||
this.$fillEntityChangesButton = this.$widget.find(".fill-entity-changes-button");
|
||||
this.$forceFullSyncButton.on("click", async () => {
|
||||
await server.post("sync/force-full-sync");
|
||||
|
||||
toastService.showMessage(t("sync.full_sync_triggered"));
|
||||
});
|
||||
|
||||
this.$fillEntityChangesButton.on("click", async () => {
|
||||
toastService.showMessage(t("sync.filling_entity_changes"));
|
||||
|
||||
await server.post("sync/fill-entity-changes");
|
||||
|
||||
toastService.showMessage(t("sync.sync_rows_filled_successfully"));
|
||||
});
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("vacuum_database.title")}</h4>
|
||||
|
||||
<p class="form-text">${t("vacuum_database.description")}</p>
|
||||
|
||||
<button class="vacuum-database-button btn btn-secondary">${t("vacuum_database.button_text")}</button>
|
||||
</div>`;
|
||||
|
||||
export default class VacuumDatabaseOptions extends OptionsWidget {
|
||||
private $vacuumDatabaseButton!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$vacuumDatabaseButton = this.$widget.find(".vacuum-database-button");
|
||||
this.$vacuumDatabaseButton.on("click", async () => {
|
||||
toastService.showMessage(t("vacuum_database.vacuuming_database"));
|
||||
|
||||
await server.post("database/vacuum-database");
|
||||
|
||||
toastService.showMessage(t("vacuum_database.database_vacuumed"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import AiSettingsWidget from './ai_settings/index.js';
|
||||
export default AiSettingsWidget;
|
||||
@@ -1,236 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import toast from "../../../services/toast";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import FormSelect from "../../react/FormSelect";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import type { OllamaModelResponse, OpenAiOrAnthropicModelResponse, OptionNames } from "@triliumnext/commons";
|
||||
import server from "../../../services/server";
|
||||
import Button from "../../react/Button";
|
||||
import FormTextArea from "../../react/FormTextArea";
|
||||
|
||||
export default function AiSettings() {
|
||||
return (
|
||||
<>
|
||||
<EnableAiSettings />
|
||||
<ProviderSettings />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function EnableAiSettings() {
|
||||
const [ aiEnabled, setAiEnabled ] = useTriliumOptionBool("aiEnabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<OptionsSection title={t("ai_llm.title")}>
|
||||
<FormGroup name="ai-enabled" description={t("ai_llm.enable_ai_description")}>
|
||||
<FormCheckbox
|
||||
label={t("ai_llm.enable_ai_features")}
|
||||
currentValue={aiEnabled} onChange={(isEnabled) => {
|
||||
if (isEnabled) {
|
||||
toast.showMessage(t("ai_llm.ai_enabled"));
|
||||
} else {
|
||||
toast.showMessage(t("ai_llm.ai_disabled"));
|
||||
}
|
||||
|
||||
setAiEnabled(isEnabled);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
{aiEnabled && <Admonition type="warning">{t("ai_llm.experimental_warning")}</Admonition>}
|
||||
</OptionsSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderSettings() {
|
||||
const [ aiSelectedProvider, setAiSelectedProvider ] = useTriliumOption("aiSelectedProvider");
|
||||
const [ aiTemperature, setAiTemperature ] = useTriliumOption("aiTemperature");
|
||||
const [ aiSystemPrompt, setAiSystemPrompt ] = useTriliumOption("aiSystemPrompt");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("ai_llm.provider_configuration")}>
|
||||
<FormGroup name="selected-provider" label={t("ai_llm.selected_provider")} description={t("ai_llm.selected_provider_description")}>
|
||||
<FormSelect
|
||||
values={[
|
||||
{ value: "", text: t("ai_llm.select_provider") },
|
||||
{ value: "openai", text: "OpenAI" },
|
||||
{ value: "anthropic", text: "Anthropic" },
|
||||
{ value: "ollama", text: "Ollama" }
|
||||
]}
|
||||
currentValue={aiSelectedProvider} onChange={setAiSelectedProvider}
|
||||
keyProperty="value" titleProperty="text"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
aiSelectedProvider === "openai" ?
|
||||
<SingleProviderSettings
|
||||
title={t("ai_llm.openai_settings")}
|
||||
apiKeyDescription={t("ai_llm.openai_api_key_description")}
|
||||
baseUrlDescription={t("ai_llm.openai_url_description")}
|
||||
modelDescription={t("ai_llm.openai_model_description")}
|
||||
validationErrorMessage={t("ai_llm.empty_key_warning.openai")}
|
||||
apiKeyOption="openaiApiKey" baseUrlOption="openaiBaseUrl" modelOption="openaiDefaultModel"
|
||||
provider={aiSelectedProvider}
|
||||
/>
|
||||
: aiSelectedProvider === "anthropic" ?
|
||||
<SingleProviderSettings
|
||||
title={t("ai_llm.anthropic_settings")}
|
||||
apiKeyDescription={t("ai_llm.anthropic_api_key_description")}
|
||||
modelDescription={t("ai_llm.anthropic_model_description")}
|
||||
baseUrlDescription={t("ai_llm.anthropic_url_description")}
|
||||
validationErrorMessage={t("ai_llm.empty_key_warning.anthropic")}
|
||||
apiKeyOption="anthropicApiKey" baseUrlOption="anthropicBaseUrl" modelOption="anthropicDefaultModel"
|
||||
provider={aiSelectedProvider}
|
||||
/>
|
||||
: aiSelectedProvider === "ollama" ?
|
||||
<SingleProviderSettings
|
||||
title={t("ai_llm.ollama_settings")}
|
||||
baseUrlDescription={t("ai_llm.ollama_url_description")}
|
||||
modelDescription={t("ai_llm.ollama_model_description")}
|
||||
validationErrorMessage={t("ai_llm.ollama_no_url")}
|
||||
baseUrlOption="ollamaBaseUrl"
|
||||
provider={aiSelectedProvider} modelOption="ollamaDefaultModel"
|
||||
/>
|
||||
:
|
||||
<></>
|
||||
}
|
||||
|
||||
<FormGroup name="ai-temperature" label={t("ai_llm.temperature")} description={t("ai_llm.temperature_description")}>
|
||||
<FormTextBox
|
||||
type="number" min="0" max="2" step="0.1"
|
||||
currentValue={aiTemperature} onChange={setAiTemperature}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="system-prompt" label={t("ai_llm.system_prompt")} description={t("ai_llm.system_prompt_description")}>
|
||||
<FormTextArea
|
||||
rows={3}
|
||||
currentValue={aiSystemPrompt} onBlur={setAiSystemPrompt}
|
||||
/>
|
||||
</FormGroup>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
interface SingleProviderSettingsProps {
|
||||
provider: string;
|
||||
title: string;
|
||||
apiKeyDescription?: string;
|
||||
baseUrlDescription: string;
|
||||
modelDescription: string;
|
||||
validationErrorMessage: string;
|
||||
apiKeyOption?: OptionNames;
|
||||
baseUrlOption: OptionNames;
|
||||
modelOption: OptionNames;
|
||||
}
|
||||
|
||||
function SingleProviderSettings({ provider, title, apiKeyDescription, baseUrlDescription, modelDescription, validationErrorMessage, apiKeyOption, baseUrlOption, modelOption }: SingleProviderSettingsProps) {
|
||||
const [ apiKey, setApiKey ] = apiKeyOption ? useTriliumOption(apiKeyOption) : [];
|
||||
const [ baseUrl, setBaseUrl ] = useTriliumOption(baseUrlOption);
|
||||
const isValid = (apiKeyOption ? !!apiKey : !!baseUrl);
|
||||
|
||||
return (
|
||||
<div class="provider-settings">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5>{title}</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
{!isValid && <Admonition type="caution">{validationErrorMessage}</Admonition> }
|
||||
|
||||
{apiKeyOption && (
|
||||
<FormGroup name="api-key" label={t("ai_llm.api_key")} description={apiKeyDescription}>
|
||||
<FormTextBox
|
||||
type="password" autoComplete="off"
|
||||
currentValue={apiKey} onChange={setApiKey}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup name="base-url" label={t("ai_llm.url")} description={baseUrlDescription}>
|
||||
<FormTextBox
|
||||
currentValue={baseUrl ?? "https://api.openai.com/v1"} onChange={setBaseUrl}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{isValid &&
|
||||
<FormGroup name="model" label={t("ai_llm.model")} description={modelDescription}>
|
||||
<ModelSelector provider={provider} baseUrl={baseUrl} modelOption={modelOption} />
|
||||
</FormGroup>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ModelSelector({ provider, baseUrl, modelOption }: { provider: string; baseUrl: string, modelOption: OptionNames }) {
|
||||
const [ model, setModel ] = useTriliumOption(modelOption);
|
||||
const [ models, setModels ] = useState<{ name: string, id: string }[]>([]);
|
||||
|
||||
const loadProviders = useCallback(async () => {
|
||||
switch (provider) {
|
||||
case "openai":
|
||||
case "anthropic": {
|
||||
try {
|
||||
const response = await server.get<OpenAiOrAnthropicModelResponse>(`llm/providers/${provider}/models?baseUrl=${encodeURIComponent(baseUrl)}`);
|
||||
if (response.success) {
|
||||
setModels(response.chatModels.toSorted((a, b) => a.name.localeCompare(b.name)));
|
||||
} else {
|
||||
toast.showError(t("ai_llm.no_models_found_online"));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.showError(t("ai_llm.error_fetching", { error: e }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ollama": {
|
||||
try {
|
||||
const response = await server.get<OllamaModelResponse>(`llm/providers/ollama/models?baseUrl=${encodeURIComponent(baseUrl)}`);
|
||||
if (response.success) {
|
||||
setModels(response.models
|
||||
.map(model => ({
|
||||
name: model.name,
|
||||
id: model.model
|
||||
}))
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name)));
|
||||
} else {
|
||||
toast.showError(t("ai_llm.no_models_found_ollama"));
|
||||
}
|
||||
} catch (e) {
|
||||
toast.showError(t("ai_llm.error_fetching", { error: e }));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [provider]);
|
||||
|
||||
useEffect(() => {
|
||||
loadProviders();
|
||||
}, [provider]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormSelect
|
||||
values={models}
|
||||
keyProperty="id" titleProperty="name"
|
||||
currentValue={model} onChange={setModel}
|
||||
/>
|
||||
|
||||
<Button
|
||||
text={t("ai_llm.refresh_models")}
|
||||
onClick={loadProviders}
|
||||
size="small"
|
||||
style={{ marginTop: "0.5em" }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { TPL } from "./template.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionDefinitions, OptionMap } from "@triliumnext/commons";
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import { ProviderService } from "./providers.js";
|
||||
|
||||
export default class AiSettingsWidget extends OptionsWidget {
|
||||
private ollamaModelsRefreshed = false;
|
||||
private openaiModelsRefreshed = false;
|
||||
private anthropicModelsRefreshed = false;
|
||||
private providerService: ProviderService | null = null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.providerService = new ProviderService(this.$widget);
|
||||
|
||||
// Setup event handlers for options
|
||||
this.setupEventHandlers();
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set up a change event handler for an option
|
||||
* @param selector The jQuery selector for the element
|
||||
* @param optionName The name of the option to update
|
||||
* @param validateAfter Whether to run validation after the update
|
||||
* @param isCheckbox Whether the element is a checkbox
|
||||
*/
|
||||
setupChangeHandler(selector: string, optionName: keyof OptionDefinitions, validateAfter: boolean = false, isCheckbox: boolean = false) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $element = this.$widget.find(selector);
|
||||
$element.on('change', async () => {
|
||||
let value: string;
|
||||
|
||||
if (isCheckbox) {
|
||||
value = $element.prop('checked') ? 'true' : 'false';
|
||||
} else {
|
||||
value = $element.val() as string;
|
||||
}
|
||||
|
||||
await this.updateOption(optionName, value);
|
||||
|
||||
// Special handling for aiEnabled option
|
||||
if (optionName === 'aiEnabled') {
|
||||
try {
|
||||
const isEnabled = value === 'true';
|
||||
|
||||
if (isEnabled) {
|
||||
toastService.showMessage(t("ai_llm.ai_enabled") || "AI features enabled");
|
||||
} else {
|
||||
toastService.showMessage(t("ai_llm.ai_disabled") || "AI features disabled");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling AI:', error);
|
||||
toastService.showError(t("ai_llm.ai_toggle_error") || "Error toggling AI features");
|
||||
}
|
||||
}
|
||||
|
||||
if (validateAfter) {
|
||||
await this.displayValidationWarnings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up all event handlers for options
|
||||
*/
|
||||
setupEventHandlers() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// Core AI options
|
||||
this.setupChangeHandler('.ai-enabled', 'aiEnabled', true, true);
|
||||
this.setupChangeHandler('.ai-selected-provider', 'aiSelectedProvider', true);
|
||||
this.setupChangeHandler('.ai-temperature', 'aiTemperature');
|
||||
this.setupChangeHandler('.ai-system-prompt', 'aiSystemPrompt');
|
||||
|
||||
// OpenAI options
|
||||
this.setupChangeHandler('.openai-api-key', 'openaiApiKey', true);
|
||||
this.setupChangeHandler('.openai-base-url', 'openaiBaseUrl', true);
|
||||
this.setupChangeHandler('.openai-default-model', 'openaiDefaultModel');
|
||||
|
||||
// Anthropic options
|
||||
this.setupChangeHandler('.anthropic-api-key', 'anthropicApiKey', true);
|
||||
this.setupChangeHandler('.anthropic-default-model', 'anthropicDefaultModel');
|
||||
this.setupChangeHandler('.anthropic-base-url', 'anthropicBaseUrl');
|
||||
|
||||
// Voyage options
|
||||
this.setupChangeHandler('.voyage-api-key', 'voyageApiKey');
|
||||
|
||||
// Ollama options
|
||||
this.setupChangeHandler('.ollama-base-url', 'ollamaBaseUrl');
|
||||
this.setupChangeHandler('.ollama-default-model', 'ollamaDefaultModel');
|
||||
|
||||
const $refreshModels = this.$widget.find('.refresh-models');
|
||||
$refreshModels.on('click', async () => {
|
||||
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(true, this.ollamaModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for Ollama tab
|
||||
const $ollamaTab = this.$widget.find('#nav-ollama-tab');
|
||||
$ollamaTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(false, this.ollamaModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// OpenAI models refresh button
|
||||
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
|
||||
$refreshOpenAIModels.on('click', async () => {
|
||||
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(true, this.openaiModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for OpenAI tab
|
||||
const $openaiTab = this.$widget.find('#nav-openai-tab');
|
||||
$openaiTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(false, this.openaiModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Anthropic models refresh button
|
||||
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
|
||||
$refreshAnthropicModels.on('click', async () => {
|
||||
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(true, this.anthropicModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for Anthropic tab
|
||||
const $anthropicTab = this.$widget.find('#nav-anthropic-tab');
|
||||
$anthropicTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(false, this.anthropicModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
|
||||
// Add provider selection change handlers for dynamic settings visibility
|
||||
this.$widget.find('.ai-selected-provider').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
this.$widget.find('.provider-settings').hide();
|
||||
if (selectedProvider) {
|
||||
this.$widget.find(`.${selectedProvider}-provider-settings`).show();
|
||||
// Automatically fetch models for the newly selected provider
|
||||
await this.fetchModelsForProvider(selectedProvider, 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add base URL change handlers to trigger model fetching
|
||||
this.$widget.find('.openai-base-url').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
if (selectedProvider === 'openai') {
|
||||
await this.fetchModelsForProvider('openai', 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find('.anthropic-base-url').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
if (selectedProvider === 'anthropic') {
|
||||
await this.fetchModelsForProvider('anthropic', 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find('.ollama-base-url').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
if (selectedProvider === 'ollama') {
|
||||
await this.fetchModelsForProvider('ollama', 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
// Add API key change handlers to trigger model fetching
|
||||
this.$widget.find('.openai-api-key').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
if (selectedProvider === 'openai') {
|
||||
await this.fetchModelsForProvider('openai', 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find('.anthropic-api-key').on('change', async () => {
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
if (selectedProvider === 'anthropic') {
|
||||
await this.fetchModelsForProvider('anthropic', 'chat');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Display warnings for validation issues with providers
|
||||
*/
|
||||
async displayValidationWarnings() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $warningDiv = this.$widget.find('.provider-validation-warning');
|
||||
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = this.$widget.find('.ai-enabled').prop('checked');
|
||||
if (!aiEnabled) {
|
||||
$warningDiv.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get selected provider
|
||||
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
|
||||
// Start with experimental warning
|
||||
const allWarnings = [
|
||||
t("ai_llm.experimental_warning")
|
||||
];
|
||||
|
||||
// Check for selected provider configuration
|
||||
const providerWarnings: string[] = [];
|
||||
if (selectedProvider === 'openai') {
|
||||
const openaiApiKey = this.$widget.find('.openai-api-key').val();
|
||||
if (!openaiApiKey) {
|
||||
providerWarnings.push(t("ai_llm.empty_key_warning.openai"));
|
||||
}
|
||||
} else if (selectedProvider === 'anthropic') {
|
||||
const anthropicApiKey = this.$widget.find('.anthropic-api-key').val();
|
||||
if (!anthropicApiKey) {
|
||||
providerWarnings.push(t("ai_llm.empty_key_warning.anthropic"));
|
||||
}
|
||||
} else if (selectedProvider === 'ollama') {
|
||||
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val();
|
||||
if (!ollamaBaseUrl) {
|
||||
providerWarnings.push(t("ai_llm.ollama_no_url"));
|
||||
}
|
||||
}
|
||||
|
||||
// Add provider warnings to all warnings
|
||||
allWarnings.push(...providerWarnings);
|
||||
|
||||
// Show or hide warnings
|
||||
if (allWarnings.length > 0) {
|
||||
const warningHtml = '<strong>' + t("ai_llm.configuration_warnings") + '</strong><ul>' +
|
||||
allWarnings.map(warning => `<li>${warning}</li>`).join('') + '</ul>';
|
||||
$warningDiv.html(warningHtml).show();
|
||||
} else {
|
||||
$warningDiv.hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper to get display name for providers
|
||||
*/
|
||||
getProviderDisplayName(provider: string): string {
|
||||
switch(provider) {
|
||||
case 'openai': return 'OpenAI';
|
||||
case 'anthropic': return 'Anthropic';
|
||||
case 'ollama': return 'Ollama';
|
||||
case 'voyage': return 'Voyage';
|
||||
case 'local': return 'Local';
|
||||
default: return provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set model dropdown value, adding the option if it doesn't exist
|
||||
*/
|
||||
setModelDropdownValue(selector: string, value: string | undefined) {
|
||||
if (!this.$widget || !value) return;
|
||||
|
||||
const $dropdown = this.$widget.find(selector);
|
||||
|
||||
// Check if the value already exists as an option
|
||||
if ($dropdown.find(`option[value="${value}"]`).length === 0) {
|
||||
// Add the custom value as an option
|
||||
$dropdown.append(`<option value="${value}">${value} (current)</option>`);
|
||||
}
|
||||
|
||||
// Set the value
|
||||
$dropdown.val(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch models for a specific provider and model type
|
||||
*/
|
||||
async fetchModelsForProvider(provider: string, modelType: 'chat') {
|
||||
if (!this.providerService) return;
|
||||
|
||||
try {
|
||||
switch (provider) {
|
||||
case 'openai':
|
||||
this.openaiModelsRefreshed = await this.providerService.refreshOpenAIModels(false, this.openaiModelsRefreshed);
|
||||
break;
|
||||
case 'anthropic':
|
||||
this.anthropicModelsRefreshed = await this.providerService.refreshAnthropicModels(false, this.anthropicModelsRefreshed);
|
||||
break;
|
||||
case 'ollama':
|
||||
this.ollamaModelsRefreshed = await this.providerService.refreshOllamaModels(false, this.ollamaModelsRefreshed);
|
||||
break;
|
||||
default:
|
||||
console.log(`Model fetching not implemented for provider: ${provider}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching models for ${provider}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update provider settings visibility based on selected providers
|
||||
*/
|
||||
updateProviderSettingsVisibility() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// Update AI provider settings visibility
|
||||
const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
this.$widget.find('.provider-settings').hide();
|
||||
if (selectedAiProvider) {
|
||||
this.$widget.find(`.${selectedAiProvider}-provider-settings`).show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the options have been loaded from the server
|
||||
*/
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// AI Options
|
||||
this.$widget.find('.ai-enabled').prop('checked', options.aiEnabled !== 'false');
|
||||
this.$widget.find('.ai-temperature').val(options.aiTemperature || '0.7');
|
||||
this.$widget.find('.ai-system-prompt').val(options.aiSystemPrompt || '');
|
||||
this.$widget.find('.ai-selected-provider').val(options.aiSelectedProvider || 'openai');
|
||||
|
||||
// OpenAI Section
|
||||
this.$widget.find('.openai-api-key').val(options.openaiApiKey || '');
|
||||
this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai.com/v1');
|
||||
this.setModelDropdownValue('.openai-default-model', options.openaiDefaultModel);
|
||||
|
||||
// Anthropic Section
|
||||
this.$widget.find('.anthropic-api-key').val(options.anthropicApiKey || '');
|
||||
this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com');
|
||||
this.setModelDropdownValue('.anthropic-default-model', options.anthropicDefaultModel);
|
||||
|
||||
// Voyage Section
|
||||
this.$widget.find('.voyage-api-key').val(options.voyageApiKey || '');
|
||||
|
||||
// Ollama Section
|
||||
this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434');
|
||||
this.setModelDropdownValue('.ollama-default-model', options.ollamaDefaultModel);
|
||||
|
||||
// Show/hide provider settings based on selected providers
|
||||
this.updateProviderSettingsVisibility();
|
||||
|
||||
// Automatically fetch models for currently selected providers
|
||||
const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string;
|
||||
|
||||
if (selectedAiProvider) {
|
||||
await this.fetchModelsForProvider(selectedAiProvider, 'chat');
|
||||
}
|
||||
|
||||
// Display validation warnings
|
||||
this.displayValidationWarnings();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Cleanup method for widget
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import AiSettingsWidget from './ai_settings_widget.js';
|
||||
export default AiSettingsWidget;
|
||||
@@ -0,0 +1,31 @@
|
||||
// Interface for the Ollama model response
|
||||
export interface OllamaModelResponse {
|
||||
success: boolean;
|
||||
models: Array<{
|
||||
name: string;
|
||||
model: string;
|
||||
details?: {
|
||||
family?: string;
|
||||
parameter_size?: string;
|
||||
}
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
export interface OpenAIModelResponse {
|
||||
success: boolean;
|
||||
chatModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AnthropicModelResponse {
|
||||
success: boolean;
|
||||
chatModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import options from "../../../../services/options.js";
|
||||
import type { OpenAIModelResponse, AnthropicModelResponse, OllamaModelResponse } from "./interfaces.js";
|
||||
|
||||
export class ProviderService {
|
||||
constructor(private $widget: JQuery<HTMLElement>) {
|
||||
// AI provider settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the dropdown has the correct value set, prioritizing:
|
||||
* 1. Current UI value if present
|
||||
* 2. Value from database options if available
|
||||
* 3. Falling back to first option if neither is available
|
||||
*/
|
||||
private ensureSelectedValue($select: JQuery<HTMLElement>, currentValue: string | number | string[] | undefined | null, optionName: string) {
|
||||
if (currentValue) {
|
||||
$select.val(currentValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$select.val()) {
|
||||
$select.prop('selectedIndex', 0);
|
||||
}
|
||||
} else {
|
||||
// If no current value exists in the dropdown but there's a default in the database
|
||||
const savedModel = options.get(optionName);
|
||||
if (savedModel) {
|
||||
$select.val(savedModel);
|
||||
// If the saved model isn't in the dropdown, select the first option
|
||||
if (!$select.val()) {
|
||||
$select.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of OpenAI models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param openaiModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshOpenAIModels(showLoading: boolean, openaiModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (openaiModelsRefreshed && !showLoading) {
|
||||
return openaiModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshOpenAIModels.prop('disabled', true);
|
||||
$refreshOpenAIModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
|
||||
}
|
||||
|
||||
try {
|
||||
const openaiBaseUrl = this.$widget.find('.openai-base-url').val() as string;
|
||||
const response = await server.get<OpenAIModelResponse>(`llm/providers/openai/models?baseUrl=${encodeURIComponent(openaiBaseUrl)}`);
|
||||
|
||||
if (response && response.success) {
|
||||
// Update the chat models dropdown
|
||||
if (response.chatModels?.length > 0) {
|
||||
const $chatModelSelect = this.$widget.find('.openai-default-model');
|
||||
const currentChatValue = $chatModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$chatModelSelect.empty();
|
||||
|
||||
// Sort models by name
|
||||
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add models to the dropdown
|
||||
sortedChatModels.forEach(model => {
|
||||
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
this.ensureSelectedValue($chatModelSelect, currentChatValue, 'openaiDefaultModel');
|
||||
}
|
||||
|
||||
|
||||
if (showLoading) {
|
||||
// Show success message
|
||||
const totalModels = (response.chatModels?.length || 0);
|
||||
toastService.showMessage(`${totalModels} OpenAI models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No OpenAI models found. Please check your API key and settings.`);
|
||||
}
|
||||
|
||||
return openaiModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching OpenAI models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching OpenAI models: ${e}`);
|
||||
}
|
||||
return openaiModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshOpenAIModels.prop('disabled', false);
|
||||
$refreshOpenAIModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of Anthropic models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param anthropicModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshAnthropicModels(showLoading: boolean, anthropicModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (anthropicModelsRefreshed && !showLoading) {
|
||||
return anthropicModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshAnthropicModels.prop('disabled', true);
|
||||
$refreshAnthropicModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
|
||||
}
|
||||
|
||||
try {
|
||||
const anthropicBaseUrl = this.$widget.find('.anthropic-base-url').val() as string;
|
||||
const response = await server.get<AnthropicModelResponse>(`llm/providers/anthropic/models?baseUrl=${encodeURIComponent(anthropicBaseUrl)}`);
|
||||
|
||||
if (response && response.success) {
|
||||
// Update the chat models dropdown
|
||||
if (response.chatModels?.length > 0) {
|
||||
const $chatModelSelect = this.$widget.find('.anthropic-default-model');
|
||||
const currentChatValue = $chatModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$chatModelSelect.empty();
|
||||
|
||||
// Sort models by name
|
||||
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add models to the dropdown
|
||||
sortedChatModels.forEach(model => {
|
||||
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
this.ensureSelectedValue($chatModelSelect, currentChatValue, 'anthropicDefaultModel');
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
// Show success message
|
||||
const totalModels = (response.chatModels?.length || 0);
|
||||
toastService.showMessage(`${totalModels} Anthropic models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No Anthropic models found. Please check your API key and settings.`);
|
||||
}
|
||||
|
||||
return anthropicModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching Anthropic models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching Anthropic models: ${e}`);
|
||||
}
|
||||
return anthropicModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshAnthropicModels.prop('disabled', false);
|
||||
$refreshAnthropicModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of Ollama models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param ollamaModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshOllamaModels(showLoading: boolean, ollamaModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshModels = this.$widget.find('.refresh-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (ollamaModelsRefreshed && !showLoading) {
|
||||
return ollamaModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshModels.prop('disabled', true);
|
||||
$refreshModels.text(t("ai_llm.refreshing_models"));
|
||||
}
|
||||
|
||||
try {
|
||||
// Use the general Ollama base URL
|
||||
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val() as string;
|
||||
|
||||
const response = await server.get<OllamaModelResponse>(`llm/providers/ollama/models?baseUrl=${encodeURIComponent(ollamaBaseUrl)}`);
|
||||
|
||||
if (response && response.success && response.models && response.models.length > 0) {
|
||||
// Update the LLM model dropdown
|
||||
const $modelSelect = this.$widget.find('.ollama-default-model');
|
||||
const currentModelValue = $modelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$modelSelect.empty();
|
||||
|
||||
// Sort models by name to make them easier to find
|
||||
const sortedModels = [...response.models].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add all models to the dropdown
|
||||
sortedModels.forEach(model => {
|
||||
$modelSelect.append(`<option value="${model.name}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
this.ensureSelectedValue($modelSelect, currentModelValue, 'ollamaDefaultModel');
|
||||
|
||||
if (showLoading) {
|
||||
toastService.showMessage(`${response.models.length} Ollama models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No Ollama models found. Please check if Ollama is running.`);
|
||||
}
|
||||
|
||||
return ollamaModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching Ollama models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching Ollama models: ${e}`);
|
||||
}
|
||||
return ollamaModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshModels.prop('disabled', false);
|
||||
$refreshModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
|
||||
export const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("ai_llm.title")}</h4>
|
||||
|
||||
<!-- Add warning alert div -->
|
||||
<div class="provider-validation-warning alert alert-warning" style="display: none;"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="tn-checkbox">
|
||||
<input class="ai-enabled form-check-input" type="checkbox">
|
||||
${t("ai_llm.enable_ai_features")}
|
||||
</label>
|
||||
<div class="form-text">${t("ai_llm.enable_ai_description")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI settings template -->
|
||||
|
||||
<div class="ai-providers-section options-section">
|
||||
<h4>${t("ai_llm.provider_configuration")}</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.selected_provider")}</label>
|
||||
<select class="ai-selected-provider form-control">
|
||||
<option value="">${t("ai_llm.select_provider")}</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
</select>
|
||||
<div class="form-text">${t("ai_llm.selected_provider_description")}</div>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI Provider Settings -->
|
||||
<div class="provider-settings openai-provider-settings" style="display: none;">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5>${t("ai_llm.openai_settings")}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.api_key")}</label>
|
||||
<input type="password" class="openai-api-key form-control" autocomplete="off" />
|
||||
<div class="form-text">${t("ai_llm.openai_api_key_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.url")}</label>
|
||||
<input type="text" class="openai-base-url form-control" />
|
||||
<div class="form-text">${t("ai_llm.openai_url_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.model")}</label>
|
||||
<select class="openai-default-model form-control">
|
||||
<option value="">${t("ai_llm.select_model")}</option>
|
||||
</select>
|
||||
<div class="form-text">${t("ai_llm.openai_model_description")}</div>
|
||||
<button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anthropic Provider Settings -->
|
||||
<div class="provider-settings anthropic-provider-settings" style="display: none;">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5>${t("ai_llm.anthropic_settings")}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.api_key")}</label>
|
||||
<input type="password" class="anthropic-api-key form-control" autocomplete="off" />
|
||||
<div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.url")}</label>
|
||||
<input type="text" class="anthropic-base-url form-control" />
|
||||
<div class="form-text">${t("ai_llm.anthropic_url_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.model")}</label>
|
||||
<select class="anthropic-default-model form-control">
|
||||
<option value="">${t("ai_llm.select_model")}</option>
|
||||
</select>
|
||||
<div class="form-text">${t("ai_llm.anthropic_model_description")}</div>
|
||||
<button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ollama Provider Settings -->
|
||||
<div class="provider-settings ollama-provider-settings" style="display: none;">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5>${t("ai_llm.ollama_settings")}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.url")}</label>
|
||||
<input type="text" class="ollama-base-url form-control" />
|
||||
<div class="form-text">${t("ai_llm.ollama_url_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.model")}</label>
|
||||
<select class="ollama-default-model form-control">
|
||||
<option value="">${t("ai_llm.select_model")}</option>
|
||||
</select>
|
||||
<div class="form-text">${t("ai_llm.ollama_model_description")}</div>
|
||||
<button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.temperature")}</label>
|
||||
<input class="ai-temperature form-control" type="number" min="0" max="2" step="0.1">
|
||||
<div class="form-text">${t("ai_llm.temperature_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.system_prompt")}</label>
|
||||
<textarea class="ai-system-prompt form-control" rows="3"></textarea>
|
||||
<div class="form-text">${t("ai_llm.system_prompt_description")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
@@ -1,270 +0,0 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils";
|
||||
import Column from "../../react/Column";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import server from "../../../services/server";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import { FontFamily, OptionNames } from "@triliumnext/commons";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormText from "../../react/FormText";
|
||||
import Button from "../../react/Button";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
|
||||
const MIN_CONTENT_WIDTH = 640;
|
||||
|
||||
interface Theme {
|
||||
val: string;
|
||||
title: string;
|
||||
noteId?: string;
|
||||
}
|
||||
|
||||
const BUILTIN_THEMES: Theme[] = [
|
||||
{ val: "next", title: t("theme.triliumnext") },
|
||||
{ val: "next-light", title: t("theme.triliumnext-light") },
|
||||
{ val: "next-dark", title: t("theme.triliumnext-dark") },
|
||||
{ val: "auto", title: t("theme.auto_theme") },
|
||||
{ val: "light", title: t("theme.light_theme") },
|
||||
{ val: "dark", title: t("theme.dark_theme") }
|
||||
]
|
||||
|
||||
interface FontFamilyEntry {
|
||||
value: FontFamily;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface FontGroup {
|
||||
title: string;
|
||||
items: FontFamilyEntry[];
|
||||
}
|
||||
|
||||
const FONT_FAMILIES: FontGroup[] = [
|
||||
{
|
||||
title: t("fonts.generic-fonts"),
|
||||
items: [
|
||||
{ value: "theme", label: t("fonts.theme_defined") },
|
||||
{ value: "system", label: t("fonts.system-default") },
|
||||
{ value: "serif", label: t("fonts.serif") },
|
||||
{ value: "sans-serif", label: t("fonts.sans-serif") },
|
||||
{ value: "monospace", label: t("fonts.monospace") }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("fonts.sans-serif-system-fonts"),
|
||||
items: [{ value: "Arial" }, { value: "Verdana" }, { value: "Helvetica" }, { value: "Tahoma" }, { value: "Trebuchet MS" }, { value: "Microsoft YaHei" }]
|
||||
},
|
||||
{
|
||||
title: t("fonts.serif-system-fonts"),
|
||||
items: [{ value: "Times New Roman" }, { value: "Georgia" }, { value: "Garamond" }]
|
||||
},
|
||||
{
|
||||
title: t("fonts.monospace-system-fonts"),
|
||||
items: [
|
||||
{ value: "Courier New" },
|
||||
{ value: "Brush Script MT" },
|
||||
{ value: "Impact" },
|
||||
{ value: "American Typewriter" },
|
||||
{ value: "Andalé Mono" },
|
||||
{ value: "Lucida Console" },
|
||||
{ value: "Monaco" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("fonts.handwriting-system-fonts"),
|
||||
items: [{ value: "Bradley Hand" }, { value: "Luminari" }, { value: "Comic Sans MS" }]
|
||||
}
|
||||
];
|
||||
|
||||
export default function AppearanceSettings() {
|
||||
const [ overrideThemeFonts ] = useTriliumOption("overrideThemeFonts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isMobile() && <LayoutOrientation />}
|
||||
<ApplicationTheme />
|
||||
{overrideThemeFonts === "true" && <Fonts />}
|
||||
{isElectron() && <ElectronIntegration /> }
|
||||
<MaxContentWidth />
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
title: t("settings_appearance.related_code_blocks"),
|
||||
targetPage: "_optionsTextNotes"
|
||||
},
|
||||
{
|
||||
title: t("settings_appearance.related_code_notes"),
|
||||
targetPage: "_optionsCodeNotes"
|
||||
}
|
||||
]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LayoutOrientation() {
|
||||
const [ layoutOrientation, setLayoutOrientation ] = useTriliumOption("layoutOrientation", true);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("theme.layout")}>
|
||||
<FormRadioGroup
|
||||
name="layout-orientation"
|
||||
values={[
|
||||
{
|
||||
label: t("theme.layout-vertical-title"),
|
||||
inlineDescription: t("theme.layout-vertical-description"),
|
||||
value: "vertical"
|
||||
},
|
||||
{
|
||||
label: t("theme.layout-horizontal-title"),
|
||||
inlineDescription: t("theme.layout-horizontal-description"),
|
||||
value: "horizontal"
|
||||
}
|
||||
]}
|
||||
currentValue={layoutOrientation} onChange={setLayoutOrientation}
|
||||
/>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function ApplicationTheme() {
|
||||
const [ theme, setTheme ] = useTriliumOption("theme", true);
|
||||
const [ overrideThemeFonts, setOverrideThemeFonts ] = useTriliumOptionBool("overrideThemeFonts");
|
||||
|
||||
const [ themes, setThemes ] = useState<Theme[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
server.get<Theme[]>("options/user-themes").then((userThemes) => {
|
||||
setThemes([
|
||||
...BUILTIN_THEMES,
|
||||
...userThemes
|
||||
])
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("theme.title")}>
|
||||
<div className="row">
|
||||
<FormGroup name="theme" label={t("theme.theme_label")} className="col-md-6" style={{ marginBottom: 0 }}>
|
||||
<FormSelect
|
||||
values={themes} currentValue={theme} onChange={setTheme}
|
||||
keyProperty="val" titleProperty="title"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className="side-checkbox col-md-6" name="override-theme-fonts">
|
||||
<FormCheckbox
|
||||
label={t("theme.override_theme_fonts_label")}
|
||||
currentValue={overrideThemeFonts} onChange={setOverrideThemeFonts} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function Fonts() {
|
||||
return (
|
||||
<OptionsSection title={t("fonts.fonts")}>
|
||||
<Font title={t("fonts.main_font")} fontFamilyOption="mainFontFamily" fontSizeOption="mainFontSize" />
|
||||
<Font title={t("fonts.note_tree_font")} fontFamilyOption="treeFontFamily" fontSizeOption="treeFontSize" />
|
||||
<Font title={t("fonts.note_detail_font")} fontFamilyOption="detailFontFamily" fontSizeOption="detailFontSize" />
|
||||
<Font title={t("fonts.monospace_font")} fontFamilyOption="monospaceFontFamily" fontSizeOption="monospaceFontSize" />
|
||||
|
||||
<FormText>{t("fonts.note_tree_and_detail_font_sizing")}</FormText>
|
||||
<FormText>{t("fonts.not_all_fonts_available")}</FormText>
|
||||
|
||||
<p>
|
||||
{t("fonts.apply_font_changes")} <Button text={t("fonts.reload_frontend")} size="micro" onClick={reloadFrontendApp} />
|
||||
</p>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, fontFamilyOption: OptionNames, fontSizeOption: OptionNames }) {
|
||||
const [ fontFamily, setFontFamily ] = useTriliumOption(fontFamilyOption);
|
||||
const [ fontSize, setFontSize ] = useTriliumOption(fontSizeOption);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h5>{title}</h5>
|
||||
<div className="row">
|
||||
<FormGroup name="font-family" className="col-md-4" label={t("fonts.font_family")}>
|
||||
<FormSelectWithGroups
|
||||
values={FONT_FAMILIES}
|
||||
currentValue={fontFamily} onChange={setFontFamily}
|
||||
keyProperty="value" titleProperty="label"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="font-size" className="col-md-6" label={t("fonts.size")}>
|
||||
<FormTextBoxWithUnit
|
||||
name="tree-font-size"
|
||||
type="number" min={50} max={200} step={10}
|
||||
currentValue={fontSize} onChange={setFontSize}
|
||||
unit={t("units.percentage")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ElectronIntegration() {
|
||||
const [ zoomFactor, setZoomFactor ] = useTriliumOption("zoomFactor");
|
||||
const [ nativeTitleBarVisible, setNativeTitleBarVisible ] = useTriliumOptionBool("nativeTitleBarVisible");
|
||||
const [ backgroundEffects, setBackgroundEffects ] = useTriliumOptionBool("backgroundEffects");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("electron_integration.desktop-application")}>
|
||||
<FormGroup name="zoom-factor" label={t("electron_integration.zoom-factor")} description={t("zoom_factor.description")}>
|
||||
<FormTextBox
|
||||
type="number"
|
||||
min="0.3" max="2.0" step="0.1"
|
||||
currentValue={zoomFactor} onChange={setZoomFactor}
|
||||
/>
|
||||
</FormGroup>
|
||||
<hr/>
|
||||
|
||||
<FormGroup name="native-title-bar" description={t("electron_integration.native-title-bar-description")}>
|
||||
<FormCheckbox
|
||||
label={t("electron_integration.native-title-bar")}
|
||||
currentValue={nativeTitleBarVisible} onChange={setNativeTitleBarVisible}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="background-effects" description={t("electron_integration.background-effects-description")}>
|
||||
<FormCheckbox
|
||||
label={t("electron_integration.background-effects")}
|
||||
currentValue={backgroundEffects} onChange={setBackgroundEffects}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
|
||||
function MaxContentWidth() {
|
||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("max_content_width.title")}>
|
||||
<FormText>{t("max_content_width.default_description")}</FormText>
|
||||
|
||||
<Column md={6}>
|
||||
<FormGroup name="max-content-width" label={t("max_content_width.max_width_label")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number" min={MIN_CONTENT_WIDTH} step="10"
|
||||
currentValue={maxContentWidth} onChange={setMaxContentWidth}
|
||||
unit={t("max_content_width.max_width_unit")}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Column>
|
||||
|
||||
<p>
|
||||
{t("max_content_width.apply_changes_description")} <Button text={t("max_content_width.reload_button")} size="micro" onClick={reloadFrontendApp} />
|
||||
</p>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("electron_integration.desktop-application")}</h4>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-12">
|
||||
<label for="zoom-factor-select">${t("electron_integration.zoom-factor")}</label>
|
||||
<input id="zoom-factor-select" type="number" class="zoom-factor-select form-control options-number-input" min="0.3" max="2.0" step="0.1"/>
|
||||
<p class="form-text">${t("zoom_factor.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label class="form-check tn-checkbox">
|
||||
<input type="checkbox" class="native-title-bar form-check-input" />
|
||||
${t("electron_integration.native-title-bar")}
|
||||
</label>
|
||||
<p class="form-text">
|
||||
${t("electron_integration.native-title-bar-description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-check tn-checkbox">
|
||||
<input type="checkbox" class="background-effects form-check-input" />
|
||||
${t("electron_integration.background-effects")}
|
||||
</label>
|
||||
<p class="form-text">
|
||||
${t("electron_integration.background-effects-description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class ElectronIntegrationOptions extends OptionsWidget {
|
||||
|
||||
private $zoomFactorSelect!: JQuery<HTMLElement>;
|
||||
private $nativeTitleBar!: JQuery<HTMLElement>;
|
||||
private $backgroundEffects!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$zoomFactorSelect = this.$widget.find(".zoom-factor-select");
|
||||
this.$zoomFactorSelect.on("change", () => {
|
||||
this.triggerCommand("setZoomFactorAndSave", { zoomFactor: String(this.$zoomFactorSelect.val()) });
|
||||
});
|
||||
|
||||
this.$nativeTitleBar = this.$widget.find("input.native-title-bar");
|
||||
this.$nativeTitleBar.on("change", () => this.updateCheckboxOption("nativeTitleBarVisible", this.$nativeTitleBar));
|
||||
|
||||
this.$backgroundEffects = this.$widget.find("input.background-effects");
|
||||
this.$backgroundEffects.on("change", () => this.updateCheckboxOption("backgroundEffects", this.$backgroundEffects));
|
||||
|
||||
const restartAppButton = this.$widget.find(".restart-app-button");
|
||||
restartAppButton.on("click", utils.restartDesktopApp);
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return utils.isElectron();
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.$zoomFactorSelect.val(options.zoomFactor);
|
||||
this.setCheckboxState(this.$nativeTitleBar, options.nativeTitleBarVisible);
|
||||
this.setCheckboxState(this.$backgroundEffects, options.backgroundEffects);
|
||||
}
|
||||
}
|
||||
218
apps/client/src/widgets/type_widgets/options/appearance/fonts.ts
Normal file
218
apps/client/src/widgets/type_widgets/options/appearance/fonts.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { FontFamily, OptionMap, OptionNames } from "@triliumnext/commons";
|
||||
|
||||
interface FontFamilyEntry {
|
||||
value: FontFamily;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface FontGroup {
|
||||
title: string;
|
||||
items: FontFamilyEntry[];
|
||||
}
|
||||
|
||||
const FONT_FAMILIES: FontGroup[] = [
|
||||
{
|
||||
title: t("fonts.generic-fonts"),
|
||||
items: [
|
||||
{ value: "theme", label: t("fonts.theme_defined") },
|
||||
{ value: "system", label: t("fonts.system-default") },
|
||||
{ value: "serif", label: t("fonts.serif") },
|
||||
{ value: "sans-serif", label: t("fonts.sans-serif") },
|
||||
{ value: "monospace", label: t("fonts.monospace") }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("fonts.sans-serif-system-fonts"),
|
||||
items: [{ value: "Arial" }, { value: "Verdana" }, { value: "Helvetica" }, { value: "Tahoma" }, { value: "Trebuchet MS" }, { value: "Microsoft YaHei" }]
|
||||
},
|
||||
{
|
||||
title: t("fonts.serif-system-fonts"),
|
||||
items: [{ value: "Times New Roman" }, { value: "Georgia" }, { value: "Garamond" }]
|
||||
},
|
||||
{
|
||||
title: t("fonts.monospace-system-fonts"),
|
||||
items: [
|
||||
{ value: "Courier New" },
|
||||
{ value: "Brush Script MT" },
|
||||
{ value: "Impact" },
|
||||
{ value: "American Typewriter" },
|
||||
{ value: "Andalé Mono" },
|
||||
{ value: "Lucida Console" },
|
||||
{ value: "Monaco" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("fonts.handwriting-system-fonts"),
|
||||
items: [{ value: "Bradley Hand" }, { value: "Luminari" }, { value: "Comic Sans MS" }]
|
||||
}
|
||||
];
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("fonts.fonts")}</h4>
|
||||
|
||||
<h5>${t("fonts.main_font")}</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="main-font-family">${t("fonts.font_family")}</label>
|
||||
<select id="main-font-family" class="main-font-family form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="main-font-size">${t("fonts.size")}</label>
|
||||
|
||||
<label class="input-group tn-number-unit-pair main-font-size-input-group">
|
||||
<input id="main-font-size" type="number" class="main-font-size form-control options-number-input" min="50" max="200" step="10"/>
|
||||
<span class="input-group-text">%</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>${t("fonts.note_tree_font")}</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="tree-font-family">${t("fonts.font_family")}</label>
|
||||
<select id="tree-font-family" class="tree-font-family form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="tree-font-size">${t("fonts.size")}</label>
|
||||
|
||||
<label class="input-group tn-number-unit-pair tree-font-size-input-group">
|
||||
<input id="tree-font-size" type="number" class="tree-font-size form-control options-number-input" min="50" max="200" step="10"/>
|
||||
<span class="input-group-text">%</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>${t("fonts.note_detail_font")}</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="detail-font-family">${t("fonts.font_family")}</label>
|
||||
<select id="detail-font-family" class="detail-font-family form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="detail-font-size">${t("fonts.size")}</label>
|
||||
|
||||
<label class="input-group tn-number-unit-pair detail-font-size-input-group">
|
||||
<input id="detail-font-size" type="number" class="detail-font-size form-control options-number-input" min="50" max="200" step="10"/>
|
||||
<span class="input-group-text">%</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>${t("fonts.monospace_font")}</h5>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-4">
|
||||
<label for="monospace-font-family">${t("fonts.font_family")}</label>
|
||||
<select id="monospace-font-family" class="monospace-font-family form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label for="monospace-font-size">${t("fonts.size")}</label>
|
||||
|
||||
<label class="input-group tn-number-unit-pair monospace-font-size-input-group">
|
||||
<input id="monospace-font-size" type="number" class="monospace-font-size form-control options-number-input" min="50" max="200" step="10"/>
|
||||
<span class="input-group-text">%</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="form-text">${t("fonts.note_tree_and_detail_font_sizing")}</p>
|
||||
|
||||
<p class="form-text">${t("fonts.not_all_fonts_available")}</p>
|
||||
|
||||
<p>
|
||||
${t("fonts.apply_font_changes")}
|
||||
<button class="btn btn-secondary btn-micro reload-frontend-button">${t("fonts.reload_frontend")}</button>
|
||||
</p>
|
||||
</div>`;
|
||||
|
||||
export default class FontsOptions extends OptionsWidget {
|
||||
private $mainFontSize!: JQuery<HTMLElement>;
|
||||
private $mainFontFamily!: JQuery<HTMLElement>;
|
||||
private $treeFontSize!: JQuery<HTMLElement>;
|
||||
private $treeFontFamily!: JQuery<HTMLElement>;
|
||||
private $detailFontSize!: JQuery<HTMLElement>;
|
||||
private $detailFontFamily!: JQuery<HTMLElement>;
|
||||
private $monospaceFontSize!: JQuery<HTMLElement>;
|
||||
private $monospaceFontFamily!: JQuery<HTMLElement>;
|
||||
|
||||
private _isEnabled?: boolean;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$mainFontSize = this.$widget.find(".main-font-size");
|
||||
this.$mainFontFamily = this.$widget.find(".main-font-family");
|
||||
|
||||
this.$treeFontSize = this.$widget.find(".tree-font-size");
|
||||
this.$treeFontFamily = this.$widget.find(".tree-font-family");
|
||||
|
||||
this.$detailFontSize = this.$widget.find(".detail-font-size");
|
||||
this.$detailFontFamily = this.$widget.find(".detail-font-family");
|
||||
|
||||
this.$monospaceFontSize = this.$widget.find(".monospace-font-size");
|
||||
this.$monospaceFontFamily = this.$widget.find(".monospace-font-family");
|
||||
|
||||
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return !!this._isEnabled;
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this._isEnabled = options.overrideThemeFonts === "true";
|
||||
this.toggleInt(this._isEnabled);
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$mainFontSize.val(options.mainFontSize);
|
||||
this.fillFontFamilyOptions(this.$mainFontFamily, options.mainFontFamily);
|
||||
|
||||
this.$treeFontSize.val(options.treeFontSize);
|
||||
this.fillFontFamilyOptions(this.$treeFontFamily, options.treeFontFamily);
|
||||
|
||||
this.$detailFontSize.val(options.detailFontSize);
|
||||
this.fillFontFamilyOptions(this.$detailFontFamily, options.detailFontFamily);
|
||||
|
||||
this.$monospaceFontSize.val(options.monospaceFontSize);
|
||||
this.fillFontFamilyOptions(this.$monospaceFontFamily, options.monospaceFontFamily);
|
||||
|
||||
const optionsToSave: OptionNames[] = ["mainFontFamily", "mainFontSize", "treeFontFamily", "treeFontSize", "detailFontFamily", "detailFontSize", "monospaceFontFamily", "monospaceFontSize"];
|
||||
|
||||
for (const optionName of optionsToSave) {
|
||||
const $el = (this as any)[`$${optionName}`];
|
||||
$el.on("change", () => this.updateOption(optionName, $el.val()));
|
||||
}
|
||||
}
|
||||
|
||||
fillFontFamilyOptions($select: JQuery<HTMLElement>, currentValue: string) {
|
||||
$select.empty();
|
||||
|
||||
for (const { title, items } of Object.values(FONT_FAMILIES)) {
|
||||
const $group = $("<optgroup>").attr("label", title);
|
||||
|
||||
for (const { value, label } of items) {
|
||||
$group.append(
|
||||
$("<option>")
|
||||
.attr("value", value)
|
||||
.prop("selected", value === currentValue)
|
||||
.text(label ?? value)
|
||||
);
|
||||
}
|
||||
|
||||
$select.append($group);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
|
||||
const MIN_VALUE = 640;
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("max_content_width.title")}</h4>
|
||||
|
||||
<p class="form-text">${t("max_content_width.default_description")}</p>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-6">
|
||||
<label for="max-content-width">${t("max_content_width.max_width_label")}</label>
|
||||
<label class="input-group tn-number-unit-pair">
|
||||
<input id="max-content-width" type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
|
||||
<span class="input-group-text">${t("max_content_width.max_width_unit")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
${t("max_content_width.apply_changes_description")}
|
||||
<button class="btn btn-secondary btn-micro reload-frontend-button">${t("max_content_width.reload_button")}</button>
|
||||
</p>
|
||||
</div>`;
|
||||
|
||||
export default class MaxContentWidthOptions extends OptionsWidget {
|
||||
|
||||
private $maxContentWidth!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$maxContentWidth = this.$widget.find(".max-content-width");
|
||||
|
||||
this.$maxContentWidth.on("change", async () => this.updateOption("maxContentWidth", String(this.$maxContentWidth.val())));
|
||||
|
||||
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp(t("max_content_width.reload_description")));
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.$maxContentWidth.val(Math.max(MIN_VALUE, parseInt(options.maxContentWidth, 10)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { OptionPages } from "../../content_widget";
|
||||
import OptionsWidget from "../options_widget";
|
||||
|
||||
const TPL = `\
|
||||
<div class="options-section">
|
||||
<h4>Related settings</h4>
|
||||
|
||||
<nav class="related-settings use-tn-links">
|
||||
<li>Color scheme for code blocks in text notes</li>
|
||||
<li>Color scheme for code notes</li>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.related-settings {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
interface RelatedSettingsConfig {
|
||||
items: {
|
||||
title: string;
|
||||
targetPage: OptionPages;
|
||||
}[];
|
||||
}
|
||||
|
||||
const RELATED_SETTINGS: Record<string, RelatedSettingsConfig> = {
|
||||
"_optionsAppearance": {
|
||||
items: [
|
||||
{
|
||||
title: "Color scheme for code blocks in text notes",
|
||||
targetPage: "_optionsTextNotes"
|
||||
},
|
||||
{
|
||||
title: "Color scheme for code notes",
|
||||
targetPage: "_optionsCodeNotes"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default class RelatedSettings extends OptionsWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
const config = this.noteId && RELATED_SETTINGS[this.noteId];
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $relatedSettings = this.$widget.find(".related-settings");
|
||||
$relatedSettings.empty();
|
||||
for (const item of config.items) {
|
||||
const $item = $("<li>");
|
||||
const $link = $("<a>").text(item.title);
|
||||
|
||||
$item.append($link);
|
||||
$link.attr("href", `#root/_hidden/_options/${item.targetPage}`);
|
||||
$relatedSettings.append($item);
|
||||
}
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return (!!this.noteId && this.noteId in RELATED_SETTINGS);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { OptionMap } from "@triliumnext/commons";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="options-section">
|
||||
<h4>${t("ribbon.widgets")}</h4>
|
||||
<div>
|
||||
<label class="tn-checkbox">
|
||||
<input type="checkbox" class="promoted-attributes-open-in-ribbon form-check-input">
|
||||
${t("ribbon.promoted_attributes_message")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="tn-checkbox">
|
||||
<input type="checkbox" class="edited-notes-open-in-ribbon form-check-input">
|
||||
${t("ribbon.edited_notes_message")}
|
||||
</label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class RibbonOptions extends OptionsWidget {
|
||||
|
||||
private $promotedAttributesOpenInRibbon!: JQuery<HTMLElement>;
|
||||
private $editedNotesOpenInRibbon!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$promotedAttributesOpenInRibbon = this.$widget.find(".promoted-attributes-open-in-ribbon");
|
||||
this.$promotedAttributesOpenInRibbon.on("change", () => this.updateCheckboxOption("promotedAttributesOpenInRibbon", this.$promotedAttributesOpenInRibbon));
|
||||
|
||||
this.$editedNotesOpenInRibbon = this.$widget.find(".edited-notes-open-in-ribbon");
|
||||
this.$editedNotesOpenInRibbon.on("change", () => this.updateCheckboxOption("editedNotesOpenInRibbon", this.$editedNotesOpenInRibbon));
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.setCheckboxState(this.$promotedAttributesOpenInRibbon, options.promotedAttributesOpenInRibbon);
|
||||
this.setCheckboxState(this.$editedNotesOpenInRibbon, options.editedNotesOpenInRibbon);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user