mirror of
https://github.com/zadam/trilium.git
synced 2026-04-11 06:27:43 +02:00
Compare commits
214 Commits
feat/fun-t
...
feature/ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5b248e663 | ||
|
|
1ec43722e8 | ||
|
|
88c548cc70 | ||
|
|
daafe251da | ||
|
|
147ecbccda | ||
|
|
adbe8f6c42 | ||
|
|
18aec84be5 | ||
|
|
5f68958aa7 | ||
|
|
4787f644a6 | ||
|
|
524f8df866 | ||
|
|
bb381c1349 | ||
|
|
36c31dac14 | ||
|
|
01b6926054 | ||
|
|
84cfa0a9f7 | ||
|
|
cb83c51632 | ||
|
|
97256ba291 | ||
|
|
d3c596aaa0 | ||
|
|
3d2fa57873 | ||
|
|
c435050018 | ||
|
|
14f761de36 | ||
|
|
626438d8f5 | ||
|
|
e29555a89b | ||
|
|
05da2d7a50 | ||
|
|
1124533557 | ||
|
|
878603c7b0 | ||
|
|
19583cd84a | ||
|
|
9f26d6efdc | ||
|
|
043e620231 | ||
|
|
d3dbdd4ceb | ||
|
|
0859165072 | ||
|
|
ca7ab6105d | ||
|
|
3af2b32783 | ||
|
|
8d5df7e888 | ||
|
|
126ee27505 | ||
|
|
fc2d8452b5 | ||
|
|
1b8c234f30 | ||
|
|
540b607459 | ||
|
|
ee229bd0d7 | ||
|
|
439d39d8fa | ||
|
|
8c379d03a9 | ||
|
|
ff31104b99 | ||
|
|
dfe6063929 | ||
|
|
a4b716f8c7 | ||
|
|
7efc36efef | ||
|
|
1554c9907e | ||
|
|
df46ddcf60 | ||
|
|
6fb19d0287 | ||
|
|
d702f69415 | ||
|
|
eb81e830a1 | ||
|
|
a24b9d7a38 | ||
|
|
efeaa1e895 | ||
|
|
a239eba6ce | ||
|
|
d009582252 | ||
|
|
fe710823c1 | ||
|
|
bfe593ae52 | ||
|
|
f653a22557 | ||
|
|
96e7f22520 | ||
|
|
e6d3d22db7 | ||
|
|
1258dedab3 | ||
|
|
ec15c7e63e | ||
|
|
5037eaf205 | ||
|
|
cb706453aa | ||
|
|
772ebbf929 | ||
|
|
60e1aca3b1 | ||
|
|
741ae4b070 | ||
|
|
31eaa4181d | ||
|
|
ca13a8accd | ||
|
|
78b1f119dc | ||
|
|
2908b29c0d | ||
|
|
91afa08cdc | ||
|
|
9e701645d5 | ||
|
|
d93b0442d2 | ||
|
|
ce4f9f5f01 | ||
|
|
353d638823 | ||
|
|
995a774140 | ||
|
|
c131b245bc | ||
|
|
42aabaf9b5 | ||
|
|
84cce151b8 | ||
|
|
e3e6316af7 | ||
|
|
96e64c4f17 | ||
|
|
3005917256 | ||
|
|
0fa121cdf2 | ||
|
|
3bf6215249 | ||
|
|
2ef045a66d | ||
|
|
2316f38978 | ||
|
|
b65bf12247 | ||
|
|
55291d43a6 | ||
|
|
743fe5a75d | ||
|
|
0c2fdba586 | ||
|
|
a2c5adec3d | ||
|
|
6089c8c7c6 | ||
|
|
f28f725519 | ||
|
|
22d853e0b0 | ||
|
|
0f1d395651 | ||
|
|
3a0bab217d | ||
|
|
f824cb5f15 | ||
|
|
40fd8d6d1a | ||
|
|
e37f73bce0 | ||
|
|
d1cd08972f | ||
|
|
5a13ca6409 | ||
|
|
eb3fd73415 | ||
|
|
1764fcbba2 | ||
|
|
19f3552bfc | ||
|
|
cedce6cf32 | ||
|
|
26cf215150 | ||
|
|
d21557069c | ||
|
|
28b2547229 | ||
|
|
d75f556074 | ||
|
|
eb66810e59 | ||
|
|
540b39206d | ||
|
|
5baea04c5d | ||
|
|
f5e65748a7 | ||
|
|
de84e09062 | ||
|
|
c81c88c930 | ||
|
|
2cb39ea7e3 | ||
|
|
6986963e45 | ||
|
|
dc9b0093d9 | ||
|
|
40f9927842 | ||
|
|
ff02f5f3ed | ||
|
|
22149b94a1 | ||
|
|
372d25667f | ||
|
|
21f6cc00eb | ||
|
|
620a080128 | ||
|
|
6a972aaf3d | ||
|
|
d878d6b20b | ||
|
|
ec075311f4 | ||
|
|
237c9bb62a | ||
|
|
5aa9733bd7 | ||
|
|
a157a003c5 | ||
|
|
e40869d3f8 | ||
|
|
edaecfad4d | ||
|
|
983a98ae15 | ||
|
|
20ad902feb | ||
|
|
05de9c6e41 | ||
|
|
df281cbbaa | ||
|
|
a979d11b8c | ||
|
|
f7ff9c114f | ||
|
|
807dbdd133 | ||
|
|
4aa944237f | ||
|
|
48db55e3da | ||
|
|
bd1491e6e5 | ||
|
|
ac35730e3b | ||
|
|
00023adbc0 | ||
|
|
a70142a4dc | ||
|
|
7b056fe1af | ||
|
|
467be38bd1 | ||
|
|
933054a095 | ||
|
|
f56482157c | ||
|
|
5d0c91d91d | ||
|
|
03136611a1 | ||
|
|
3e7488e4f3 | ||
|
|
3ed7d48d42 | ||
|
|
ef72d89172 | ||
|
|
ad97071862 | ||
|
|
2291892946 | ||
|
|
bf8cfa1421 | ||
|
|
bdd806efff | ||
|
|
c912c4af7b | ||
|
|
fc7f359f28 | ||
|
|
21598f6189 | ||
|
|
a1987ea193 | ||
|
|
480d167131 | ||
|
|
d873accf3e | ||
|
|
94b448863c | ||
|
|
32acc8555d | ||
|
|
d68ad84155 | ||
|
|
45e82b7f33 | ||
|
|
55ad0fe9f0 | ||
|
|
559815273e | ||
|
|
af76740fd9 | ||
|
|
7dadd50bfe | ||
|
|
dd4cab22c1 | ||
|
|
c4d3e776a1 | ||
|
|
19bb7f5ddb | ||
|
|
d212120f9b | ||
|
|
42da1872e7 | ||
|
|
a080b50c45 | ||
|
|
6d31e9b028 | ||
|
|
b606afa858 | ||
|
|
f9446304b3 | ||
|
|
fbe312d580 | ||
|
|
8d383caaff | ||
|
|
6caf4fa7ce | ||
|
|
606d58b08c | ||
|
|
09258179f0 | ||
|
|
40e986b188 | ||
|
|
37e47041bf | ||
|
|
543438bca0 | ||
|
|
b31290c1fc | ||
|
|
d41111a209 | ||
|
|
828b523382 | ||
|
|
32409ecbee | ||
|
|
3ca2cec63a | ||
|
|
1ed2db0c82 | ||
|
|
2423b74dd0 | ||
|
|
3f781ea298 | ||
|
|
30c5c49aef | ||
|
|
9421e39c34 | ||
|
|
c46805cf4f | ||
|
|
f181343fca | ||
|
|
8a512e4f73 | ||
|
|
06a3750168 | ||
|
|
35c1a5642d | ||
|
|
f29df2ad28 | ||
|
|
75a5714451 | ||
|
|
2882863b5b | ||
|
|
773b6cca14 | ||
|
|
15505ffcd8 | ||
|
|
96cef35f09 | ||
|
|
f8c59a1730 | ||
|
|
c833c3591f | ||
|
|
ccbd962e0b | ||
|
|
966d2afe69 | ||
|
|
ac24c69858 |
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@@ -1,5 +1,7 @@
|
||||
# Trilium Notes - AI Coding Agent Instructions
|
||||
|
||||
> **Note**: When updating this file, also update `CLAUDE.md` in the repository root to keep both AI coding assistants in sync.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
|
||||
@@ -115,6 +117,15 @@ class MyNoteWidget extends NoteContextAwareWidget {
|
||||
|
||||
**Important**: Widgets use jQuery (`this.$widget`) for DOM manipulation. Don't mix React patterns here.
|
||||
|
||||
### Reusable Preact Components
|
||||
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
|
||||
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
|
||||
- `ActionButton` - Consistent button styling with icon support
|
||||
- `FormTextBox` - Text input with validation and controlled input handling
|
||||
- `Slider` - Range slider with label
|
||||
- `Checkbox`, `RadioButton` - Form controls
|
||||
- `CollapsibleSection` - Expandable content sections
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Running & Testing
|
||||
@@ -320,9 +331,18 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Use translation system via `t()` function
|
||||
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
|
||||
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
|
||||
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
|
||||
|
||||
#### Client vs Server Translation Usage
|
||||
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
|
||||
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
|
||||
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
|
||||
|
||||
## Testing Conventions
|
||||
|
||||
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
|
||||
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
|
||||
|
||||
```typescript
|
||||
// ETAPI test pattern
|
||||
describe("etapi/feature", () => {
|
||||
|
||||
39
CLAUDE.md
39
CLAUDE.md
@@ -2,6 +2,8 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
> **Note**: When updating this file, also update `.github/copilot-instructions.md` to keep both AI coding assistants in sync.
|
||||
|
||||
## Overview
|
||||
|
||||
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. It's built as a TypeScript monorepo using pnpm, with multiple applications and shared packages.
|
||||
@@ -66,6 +68,15 @@ Frontend uses a widget system (`apps/client/src/widgets/`):
|
||||
- `RightPanelWidget` - Widgets displayed in the right panel
|
||||
- Type-specific widgets in `type_widgets/` directory
|
||||
|
||||
#### Reusable Preact Components
|
||||
Common UI components are available in `apps/client/src/widgets/react/` — prefer reusing these over creating custom implementations:
|
||||
- `NoItems` - Empty state placeholder with icon and message (use for "no results", "too many items", error states)
|
||||
- `ActionButton` - Consistent button styling with icon support
|
||||
- `FormTextBox` - Text input with validation and controlled input handling
|
||||
- `Slider` - Range slider with label
|
||||
- `Checkbox`, `RadioButton` - Form controls
|
||||
- `CollapsibleSection` - Expandable content sections
|
||||
|
||||
#### API Architecture
|
||||
- **Internal API**: REST endpoints in `apps/server/src/routes/api/`
|
||||
- **ETAPI**: External API for third-party integrations (`apps/server/src/etapi/`)
|
||||
@@ -108,6 +119,8 @@ Trilium supports multiple note types, each with specialized widgets:
|
||||
- Client tests can run in parallel
|
||||
- E2E tests use Playwright for both server and desktop apps
|
||||
- Build validation tests check artifact integrity
|
||||
- **Write concise tests**: Group related assertions together in a single test case rather than creating many one-shot tests
|
||||
- **Extract and test business logic**: When adding pure business logic (e.g., data transformations, migrations, validations), extract it as a separate function and always write unit tests for it
|
||||
|
||||
### Scripting System
|
||||
Trilium provides powerful user scripting capabilities:
|
||||
@@ -121,6 +134,18 @@ Trilium provides powerful user scripting capabilities:
|
||||
- **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community
|
||||
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
|
||||
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
|
||||
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
|
||||
- **Server-side translations** (e.g. hidden subtree titles) go in `apps/server/src/assets/translations/en/server.json`, not in the client `translation.json`
|
||||
|
||||
#### Client vs Server Translation Usage
|
||||
- **Client-side**: `import { t } from "../services/i18n"` with keys in `apps/client/src/translations/en/translation.json`
|
||||
- **Server-side**: `import { t } from "i18next"` with keys in `apps/server/src/assets/translations/en/server.json`
|
||||
- **Interpolation**: Use `{{variable}}` for normal interpolation; use `{{- variable}}` (with hyphen) for **unescaped** interpolation when the value contains special characters like quotes that shouldn't be HTML-escaped
|
||||
|
||||
### Electron Desktop App
|
||||
- Desktop entry point: `apps/desktop/src/main.ts`, window management: `apps/server/src/services/window.ts`
|
||||
- IPC communication: use `electron.ipcMain.on(channel, handler)` on server side, `electron.ipcRenderer.send(channel, data)` on client side
|
||||
- Electron-only features should check `isElectron()` from `apps/client/src/services/utils.ts` (client) or `utils.isElectron` (server)
|
||||
|
||||
### Security Considerations
|
||||
- Per-note encryption with granular protected sessions
|
||||
@@ -152,6 +177,20 @@ Trilium provides powerful user scripting capabilities:
|
||||
- Create new package in `packages/` following existing plugin structure
|
||||
- Register in `packages/ckeditor5/src/plugins.ts`
|
||||
|
||||
### Adding Hidden System Notes
|
||||
The hidden subtree (`_hidden`) contains system notes with predictable IDs (prefixed with `_`). Defined in `apps/server/src/services/hidden_subtree.ts` via the `HiddenSubtreeItem` interface from `@triliumnext/commons`.
|
||||
|
||||
1. Add the note definition to `buildHiddenSubtreeDefinition()` in `apps/server/src/services/hidden_subtree.ts`
|
||||
2. Add a translation key for the title in `apps/server/src/assets/translations/en/server.json` under `"hidden-subtree"`
|
||||
3. The note is auto-created on startup by `checkHiddenSubtree()` — uses deterministic IDs so all sync cluster instances generate the same structure
|
||||
4. Key properties: `id` (must start with `_`), `title`, `type`, `icon` (format: `bx-icon-name` without `bx ` prefix), `attributes`, `children`, `content`
|
||||
5. Use `enforceAttributes: true` to keep attributes in sync, `enforceBranches: true` for correct placement, `enforceDeleted: true` to remove deprecated notes
|
||||
6. For launcher bar entries, see `hidden_subtree_launcherbar.ts`; for templates, see `hidden_subtree_templates.ts`
|
||||
|
||||
### Writing to Notes from Server Services
|
||||
- `note.setContent()` requires a CLS (Continuation Local Storage) context — wrap calls in `cls.init(() => { ... })` (from `apps/server/src/services/cls.ts`)
|
||||
- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
|
||||
|
||||
### Adding New LLM Tools
|
||||
Tools are defined using `defineTools()` in `apps/server/src/services/llm/tools/` and automatically registered for both the LLM chat and MCP server.
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.33.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.25.3",
|
||||
"@redocly/cli": "2.25.4",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.4",
|
||||
"js-yaml": "4.1.1",
|
||||
"typedoc": "0.28.18",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
"typedoc-plugin-missing-exports": "4.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.19.0",
|
||||
"@univerjs/preset-sheets-core": "0.19.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.19.0",
|
||||
"@univerjs/preset-sheets-filter": "0.19.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.19.0",
|
||||
"@univerjs/preset-sheets-note": "0.19.0",
|
||||
"@univerjs/preset-sheets-sort": "0.19.0",
|
||||
"@univerjs/presets": "0.19.0",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.20.0",
|
||||
"@univerjs/preset-sheets-core": "0.20.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.20.0",
|
||||
"@univerjs/preset-sheets-filter": "0.20.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.20.0",
|
||||
"@univerjs/preset-sheets-note": "0.20.0",
|
||||
"@univerjs/preset-sheets-sort": "0.20.0",
|
||||
"@univerjs/presets": "0.20.0",
|
||||
"@zumer/snapdom": "2.7.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
@@ -57,15 +57,15 @@
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.44",
|
||||
"katex": "0.16.45",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.5",
|
||||
"marked": "17.0.6",
|
||||
"mermaid": "11.14.0",
|
||||
"mind-elixir": "5.10.0",
|
||||
"panzoom": "9.4.4",
|
||||
"preact": "10.29.0",
|
||||
"preact": "10.29.1",
|
||||
"react-i18next": "17.0.2",
|
||||
"react-window": "2.2.7",
|
||||
"reveal.js": "6.0.0",
|
||||
@@ -87,6 +87,6 @@
|
||||
"happy-dom": "20.8.9",
|
||||
"lightningcss": "1.32.0",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "4.0.0"
|
||||
"vite-plugin-static-copy": "4.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
import type { NativeImage, TouchBar } from "electron";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { initLocale, t } from "../services/i18n.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
@@ -563,7 +564,7 @@ export class AppContext extends Component {
|
||||
*/
|
||||
async earlyInit() {
|
||||
await options.initializedPromise;
|
||||
await initLocale();
|
||||
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
|
||||
}
|
||||
|
||||
setLayout(layout: Layout) {
|
||||
@@ -578,7 +579,6 @@ export class AppContext extends Component {
|
||||
|
||||
this.tabManager.loadTabs();
|
||||
|
||||
const bundleService = (await import("../services/bundle.js")).default;
|
||||
setTimeout(() => bundleService.executeStartupBundles(), 2000);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ function initOnElectron() {
|
||||
const currentWindow = electronRemote.getCurrentWindow();
|
||||
const style = window.getComputedStyle(document.body);
|
||||
|
||||
initDarkOrLightMode(style);
|
||||
initDarkOrLightMode();
|
||||
initTransparencyEffects(style, currentWindow);
|
||||
initFullScreenDetection(currentWindow);
|
||||
|
||||
@@ -119,11 +119,11 @@ function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Elec
|
||||
*
|
||||
* @param style the root CSS element to read variables from.
|
||||
*/
|
||||
function initDarkOrLightMode(style: CSSStyleDeclaration) {
|
||||
function initDarkOrLightMode() {
|
||||
let themeSource: typeof nativeTheme.themeSource = "system";
|
||||
|
||||
const themeStyle = style.getPropertyValue("--theme-style");
|
||||
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
|
||||
const themeStyle = window.glob.getThemeStyle();
|
||||
if (themeStyle !== "auto") {
|
||||
themeSource = themeStyle;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getNoteIcon } from "@triliumnext/commons";
|
||||
|
||||
import bundleService from "../services/bundle.js";
|
||||
import cssClassManager from "../services/css_class_manager.js";
|
||||
import type { Froca } from "../services/froca-interface.js";
|
||||
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||
@@ -235,6 +236,16 @@ export default class FNote {
|
||||
return this.hasAttribute("label", "archived");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the note's metadata (title, icon) should not be editable.
|
||||
* This applies to system notes like options, help, and launch bar configuration.
|
||||
*/
|
||||
get isMetadataReadOnly() {
|
||||
return utils.isLaunchBarConfig(this.noteId)
|
||||
|| this.noteId.startsWith("_help_")
|
||||
|| this.noteId.startsWith("_options");
|
||||
}
|
||||
|
||||
getChildNoteIds() {
|
||||
return this.children;
|
||||
}
|
||||
@@ -1014,7 +1025,6 @@ export default class FNote {
|
||||
const env = this.getScriptEnv();
|
||||
|
||||
if (env === "frontend") {
|
||||
const bundleService = (await import("../services/bundle.js")).default;
|
||||
return await bundleService.getAndExecuteBundle(this.noteId);
|
||||
} else if (env === "backend") {
|
||||
await server.post(`script/run/${this.noteId}`);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getThemeStyle } from "./services/theme";
|
||||
|
||||
async function bootstrap() {
|
||||
showSplash();
|
||||
await setupGlob();
|
||||
@@ -38,6 +40,7 @@ async function setupGlob() {
|
||||
...json,
|
||||
activeDialog: null
|
||||
};
|
||||
window.glob.getThemeStyle = getThemeStyle;
|
||||
}
|
||||
|
||||
async function loadBootstrapCss() {
|
||||
@@ -49,31 +52,65 @@ async function loadBootstrapCss() {
|
||||
}
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { device, assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
type StylesheetRef = {
|
||||
href: string;
|
||||
media?: string;
|
||||
};
|
||||
|
||||
const cssToLoad: string[] = [];
|
||||
if (device !== "print") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/ckeditor-theme.css`);
|
||||
cssToLoad.push(`api/fonts`);
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-light.css`);
|
||||
if (themeCssUrl) {
|
||||
cssToLoad.push(themeCssUrl);
|
||||
}
|
||||
if (themeUseNextAsBase === "next") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next.css`);
|
||||
} else if (themeUseNextAsBase === "next-dark") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-dark.css`);
|
||||
} else if (themeUseNextAsBase === "next-light") {
|
||||
cssToLoad.push(`${assetPath}/stylesheets/theme-next-light.css`);
|
||||
}
|
||||
cssToLoad.push(`${assetPath}/stylesheets/style.css`);
|
||||
function getConfiguredThemeStylesheets(stylesheetsPath: string, theme: string, customThemeCssUrl?: string) {
|
||||
if (theme === "auto") {
|
||||
return [{ href: `${stylesheetsPath}/theme-dark.css`, media: "(prefers-color-scheme: dark)" }];
|
||||
}
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
if (theme === "dark") {
|
||||
return [{ href: `${stylesheetsPath}/theme-dark.css` }];
|
||||
}
|
||||
|
||||
if (theme === "next") {
|
||||
return [
|
||||
{ href: `${stylesheetsPath}/theme-next-light.css` },
|
||||
{ href: `${stylesheetsPath}/theme-next-dark.css`, media: "(prefers-color-scheme: dark)" }
|
||||
];
|
||||
}
|
||||
|
||||
if (theme === "next-light") {
|
||||
return [{ href: `${stylesheetsPath}/theme-next-light.css` }];
|
||||
}
|
||||
|
||||
if (theme === "next-dark") {
|
||||
return [{ href: `${stylesheetsPath}/theme-next-dark.css` }];
|
||||
}
|
||||
|
||||
if (theme !== "light" && customThemeCssUrl) {
|
||||
return [{ href: customThemeCssUrl }];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { device, assetPath, theme, themeBase, customThemeCssUrl } = window.glob;
|
||||
const stylesheetsPath = `${assetPath}/stylesheets`;
|
||||
|
||||
const cssToLoad: StylesheetRef[] = [];
|
||||
if (device !== "print") {
|
||||
cssToLoad.push({ href: `${stylesheetsPath}/ckeditor-theme.css` });
|
||||
cssToLoad.push({ href: `api/fonts` });
|
||||
cssToLoad.push({ href: `${stylesheetsPath}/theme-light.css` });
|
||||
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, theme, customThemeCssUrl));
|
||||
if (themeBase) {
|
||||
cssToLoad.push(...getConfiguredThemeStylesheets(stylesheetsPath, themeBase));
|
||||
}
|
||||
cssToLoad.push({ href: `${stylesheetsPath}/style.css` });
|
||||
}
|
||||
|
||||
for (const { href, media } of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.href = href;
|
||||
linkEl.rel = "stylesheet";
|
||||
if (media) {
|
||||
linkEl.media = media;
|
||||
}
|
||||
document.head.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import utils from "../services/utils.js";
|
||||
import options from "../services/options.js";
|
||||
import zoomService from "../components/zoom.js";
|
||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import * as clipboardExt from "../services/clipboard_ext.js";
|
||||
import type { BrowserWindow } from "electron";
|
||||
import type { CommandNames, AppContext } from "../components/app_context.js";
|
||||
|
||||
import type { CommandNames } from "../components/app_context.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import zoomService from "../components/zoom.js";
|
||||
import * as clipboardExt from "../services/clipboard_ext.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import options from "../services/options.js";
|
||||
import server from "../services/server.js";
|
||||
import utils from "../services/utils.js";
|
||||
import contextMenu, { type MenuItem } from "./context_menu.js";
|
||||
|
||||
function setupContextMenu() {
|
||||
const electron = utils.dynamicRequire("electron");
|
||||
@@ -15,8 +17,6 @@ function setupContextMenu() {
|
||||
// FIXME: Remove typecast once Electron is properly integrated.
|
||||
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
|
||||
|
||||
let appContext: AppContext;
|
||||
|
||||
webContents.on("context-menu", (event, params) => {
|
||||
const { editFlags } = params;
|
||||
const hasText = params.selectionText.trim().length > 0;
|
||||
@@ -38,7 +38,7 @@ function setupContextMenu() {
|
||||
items.push({
|
||||
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
|
||||
uiIcon: "bx bx-plus",
|
||||
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
|
||||
handler: () => electron.ipcRenderer.send("add-word-to-dictionary", params.misspelledWord)
|
||||
});
|
||||
|
||||
items.push({ kind: "separator" });
|
||||
@@ -141,7 +141,7 @@ function setupContextMenu() {
|
||||
}
|
||||
|
||||
// Replace the placeholder with the real search keyword.
|
||||
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
|
||||
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
|
||||
|
||||
items.push({ kind: "separator" });
|
||||
|
||||
@@ -155,10 +155,6 @@ function setupContextMenu() {
|
||||
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
|
||||
uiIcon: "bx bx-search",
|
||||
handler: async () => {
|
||||
if (!appContext) {
|
||||
appContext = (await import("../components/app_context.js")).default;
|
||||
}
|
||||
|
||||
await appContext.triggerCommand("searchNotes", {
|
||||
searchString: params.selectionText
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import FNote from "./entities/fnote";
|
||||
import content_renderer from "./services/content_renderer";
|
||||
import { applyInlineMermaid } from "./services/content_renderer_text";
|
||||
import froca from "./services/froca";
|
||||
import { dynamicRequire, isElectron } from "./services/utils";
|
||||
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
|
||||
|
||||
@@ -30,7 +31,6 @@ async function main() {
|
||||
if (!noteId) return;
|
||||
|
||||
await import("./print.css");
|
||||
const froca = (await import("./services/froca")).default;
|
||||
const note = await froca.getNote(noteId);
|
||||
|
||||
const bodyWrapper = document.createElement("div");
|
||||
|
||||
@@ -26,7 +26,7 @@ type WithNoteId<T> = T & {
|
||||
};
|
||||
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
|
||||
|
||||
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
|
||||
async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) {
|
||||
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
|
||||
script,
|
||||
params
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { t } from "./i18n.js";
|
||||
import toast from "./toast.js";
|
||||
|
||||
export function copyText(text: string) {
|
||||
if (!text) {
|
||||
return;
|
||||
@@ -6,29 +9,26 @@ export function copyText(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} else {
|
||||
// Fallback method: https://stackoverflow.com/a/72239825
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
try {
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
return document.execCommand('copy');
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
}
|
||||
// Fallback method: https://stackoverflow.com/a/72239825
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
try {
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
return document.execCommand('copy');
|
||||
} finally {
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyTextWithToast(text: string) {
|
||||
const t = (await import("./i18n.js")).t;
|
||||
const toast = (await import("./toast.js")).default;
|
||||
|
||||
export function copyTextWithToast(text: string) {
|
||||
if (copyText(text)) {
|
||||
toast.showMessage(t("clipboard.copy_success"));
|
||||
} else {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Modal } from "bootstrap";
|
||||
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
|
||||
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "./focus.js";
|
||||
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
|
||||
import keyboardActionsService from "./keyboard_actions.js";
|
||||
|
||||
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
|
||||
if (closeActDialog) {
|
||||
@@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
|
||||
}
|
||||
});
|
||||
|
||||
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
|
||||
keyboardActionsService.updateDisplayedShortcuts($dialog);
|
||||
|
||||
return $dialog;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import LoadResults from "./load_results.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import options from "./options.js";
|
||||
import noteAttributeCache from "./note_attribute_cache.js";
|
||||
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
|
||||
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
|
||||
import type { OptionNames } from "@triliumnext/commons";
|
||||
|
||||
import appContext from "../components/app_context.js";
|
||||
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
|
||||
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
|
||||
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
|
||||
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
import type { OptionNames } from "@triliumnext/commons";
|
||||
import froca from "./froca.js";
|
||||
import LoadResults from "./load_results.js";
|
||||
import noteAttributeCache from "./note_attribute_cache.js";
|
||||
import options from "./options.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
const loadResults = new LoadResults(entityChanges);
|
||||
@@ -63,7 +65,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
|
||||
missingNoteIds.push((entity as FBranchRow).parentNoteId);
|
||||
} else if (entityName === "attributes") {
|
||||
let attributeEntity = entity as FAttributeRow;
|
||||
const attributeEntity = entity as FAttributeRow;
|
||||
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
|
||||
missingNoteIds.push(attributeEntity.value);
|
||||
}
|
||||
@@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
noteAttributeCache.invalidate();
|
||||
}
|
||||
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
await appContext.triggerEvent("entitiesReloaded", { loadResults });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, h, VNode } from "preact";
|
||||
import { createContext, Fragment, h, VNode } from "preact";
|
||||
import * as hooks from "preact/hooks";
|
||||
|
||||
import ActionButton from "../widgets/react/ActionButton";
|
||||
@@ -47,6 +47,7 @@ export const preactAPI = Object.freeze({
|
||||
// Core
|
||||
h,
|
||||
Fragment,
|
||||
createContext,
|
||||
|
||||
/**
|
||||
* Method that must be run for widget scripts that run on Preact, using JSX. The method just returns the same definition, reserved for future typechecking and perhaps validation purposes.
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import options from "./options.js";
|
||||
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
|
||||
import i18next from "i18next";
|
||||
import i18nextHttpBackend from "i18next-http-backend";
|
||||
import server from "./server.js";
|
||||
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
let locales: Locale[] | null;
|
||||
|
||||
/**
|
||||
* A deferred promise that resolves when translations are initialized.
|
||||
*/
|
||||
export let translationsInitializedPromise = $.Deferred();
|
||||
export const translationsInitializedPromise = $.Deferred();
|
||||
|
||||
export async function initLocale() {
|
||||
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
|
||||
|
||||
locales = await server.get<Locale[]>("options/locales");
|
||||
export async function initLocale(locale: LOCALE_IDS = "en") {
|
||||
|
||||
i18next.use(initReactI18next);
|
||||
await i18next.use(i18nextHttpBackend).init({
|
||||
@@ -32,11 +25,7 @@ export async function initLocale() {
|
||||
}
|
||||
|
||||
export function getAvailableLocales() {
|
||||
if (!locales) {
|
||||
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
|
||||
}
|
||||
|
||||
return locales;
|
||||
return LOCALES;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +36,7 @@ export function getAvailableLocales() {
|
||||
*/
|
||||
export function getLocaleById(localeId: string | null | undefined) {
|
||||
if (!localeId) return null;
|
||||
return locales?.find((l) => l.id === localeId) ?? null;
|
||||
return LOCALES.find((l) => l.id === localeId) ?? null;
|
||||
}
|
||||
|
||||
export const t = i18next.t;
|
||||
|
||||
@@ -68,7 +68,8 @@ async function autocompleteSourceForCKEditor(queryText: string) {
|
||||
name: row.notePathTitle || "",
|
||||
link: `#${row.notePath}`,
|
||||
notePath: row.notePath,
|
||||
highlightedNotePathTitle: row.highlightedNotePathTitle
|
||||
highlightedNotePathTitle: row.highlightedNotePathTitle,
|
||||
icon: row.icon
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "./i18n.js";
|
||||
import utils, { isShare } from "./utils.js";
|
||||
import ValidationError from "./validation_error.js";
|
||||
|
||||
@@ -32,8 +33,7 @@ async function getHeaders(headers?: Headers) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
|
||||
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
|
||||
|
||||
// headers need to be lowercase because node.js automatically converts them to lower case
|
||||
// also avoiding using underscores instead of dashes since nginx filters them out by default
|
||||
@@ -344,6 +344,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Dynamic import to avoid circular dependency (toast → app_context → options → server).
|
||||
const toastService = (await import("./toast.js")).default;
|
||||
|
||||
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
|
||||
@@ -357,7 +358,6 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
||||
...response
|
||||
});
|
||||
} else {
|
||||
const { t } = await import("./i18n.js");
|
||||
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
|
||||
toastService.showPersistent({
|
||||
id: "trafik-blocked",
|
||||
@@ -371,8 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
|
||||
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
|
||||
15_000);
|
||||
}
|
||||
const { logError } = await import("./ws.js");
|
||||
logError(`${statusCode} ${method} ${url} - ${message}`);
|
||||
window.logError(`${statusCode} ${method} ${url} - ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
87
apps/client/src/services/spaced_update.spec.ts
Normal file
87
apps/client/src/services/spaced_update.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import SpacedUpdate from "./spaced_update";
|
||||
|
||||
// Mock logError which is a global in Trilium
|
||||
vi.stubGlobal("logError", vi.fn());
|
||||
|
||||
describe("SpacedUpdate", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should only call updater once per interval even with multiple pending callbacks", async () => {
|
||||
const updater = vi.fn(async () => {
|
||||
// Simulate a slow network request - this is where the race condition occurs
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(updater, 50);
|
||||
|
||||
// Simulate rapid typing - each keystroke calls scheduleUpdate()
|
||||
// This queues multiple setTimeout callbacks due to recursive scheduleUpdate() calls
|
||||
for (let i = 0; i < 10; i++) {
|
||||
spacedUpdate.scheduleUpdate();
|
||||
// Small delay between keystrokes
|
||||
await vi.advanceTimersByTimeAsync(5);
|
||||
}
|
||||
|
||||
// Advance time past the update interval to trigger the update
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Let the "network request" complete and any pending callbacks run
|
||||
await vi.advanceTimersByTimeAsync(200);
|
||||
|
||||
// The updater should have been called only ONCE, not multiple times
|
||||
// With the bug, multiple pending setTimeout callbacks would all pass the time check
|
||||
// during the async updater call and trigger multiple concurrent requests
|
||||
expect(updater).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call updater again if changes occur during the update", async () => {
|
||||
const updater = vi.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(updater, 30);
|
||||
|
||||
// First update
|
||||
spacedUpdate.scheduleUpdate();
|
||||
await vi.advanceTimersByTimeAsync(40);
|
||||
|
||||
// Schedule another update while the first one is in progress
|
||||
spacedUpdate.scheduleUpdate();
|
||||
|
||||
// Let first update complete
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
|
||||
// Advance past the interval again for the second update
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
// Should have been called twice - once for each distinct change period
|
||||
expect(updater).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("should restore changed flag on error so retry can happen", async () => {
|
||||
const updater = vi.fn()
|
||||
.mockRejectedValueOnce(new Error("Network error"))
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
const spacedUpdate = new SpacedUpdate(updater, 50);
|
||||
|
||||
spacedUpdate.scheduleUpdate();
|
||||
|
||||
// Advance to trigger first update (which will fail)
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
|
||||
// The error should have restored the changed flag, so scheduling again should work
|
||||
spacedUpdate.scheduleUpdate();
|
||||
await vi.advanceTimersByTimeAsync(60);
|
||||
|
||||
expect(updater).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -77,16 +77,22 @@ export default class SpacedUpdate {
|
||||
}
|
||||
|
||||
if (Date.now() - this.lastUpdated > this.updateInterval) {
|
||||
// Update these BEFORE the async call to prevent race conditions.
|
||||
// Multiple setTimeout callbacks may be pending from recursive scheduleUpdate() calls.
|
||||
// Without this, they would all pass the time check during the await and trigger multiple requests.
|
||||
this.lastUpdated = Date.now();
|
||||
this.changed = false;
|
||||
|
||||
this.onStateChanged("saving");
|
||||
try {
|
||||
await this.updater();
|
||||
this.onStateChanged("saved");
|
||||
this.changed = false;
|
||||
} catch (e) {
|
||||
// Restore changed flag on error so a retry can happen
|
||||
this.changed = true;
|
||||
this.onStateChanged("error");
|
||||
logError(getErrorMessage(e));
|
||||
}
|
||||
this.lastUpdated = Date.now();
|
||||
} else {
|
||||
// update isn't triggered but changes are still pending, so we need to schedule another check
|
||||
this.scheduleUpdate();
|
||||
|
||||
@@ -33,6 +33,14 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
|
||||
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
||||
}
|
||||
}
|
||||
|
||||
// Add click-to-copy for inline code (code elements not inside pre)
|
||||
if (glob.device !== "print") {
|
||||
const inlineCodeElements = $container.find("code:not(pre code)");
|
||||
for (const inlineCode of inlineCodeElements) {
|
||||
applyInlineCodeCopy($(inlineCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
|
||||
@@ -51,6 +59,22 @@ export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
|
||||
$codeBlock.parent().append($copyButton);
|
||||
}
|
||||
|
||||
export function applyInlineCodeCopy($inlineCode: JQuery<HTMLElement>) {
|
||||
$inlineCode
|
||||
.addClass("copyable-inline-code")
|
||||
.attr("title", t("code_block.click_to_copy"))
|
||||
.on("click", (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const text = $inlineCode.text();
|
||||
if (!isShare) {
|
||||
copyTextWithToast(text);
|
||||
} else {
|
||||
copyText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
|
||||
*/
|
||||
|
||||
35
apps/client/src/services/theme.ts
Normal file
35
apps/client/src/services/theme.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export function getThemeStyle(): "auto" | "light" | "dark" {
|
||||
const configuredTheme = window.glob?.theme;
|
||||
if (configuredTheme === "auto" || configuredTheme === "next") {
|
||||
return "auto";
|
||||
}
|
||||
|
||||
if (configuredTheme === "light" || configuredTheme === "dark") {
|
||||
return configuredTheme;
|
||||
}
|
||||
|
||||
if (configuredTheme === "next-light") {
|
||||
return "light";
|
||||
}
|
||||
|
||||
if (configuredTheme === "next-dark") {
|
||||
return "dark";
|
||||
}
|
||||
|
||||
const style = window.getComputedStyle(document.body);
|
||||
const themeStyle = style.getPropertyValue("--theme-style");
|
||||
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
|
||||
return themeStyle as "light" | "dark";
|
||||
}
|
||||
|
||||
return "auto";
|
||||
}
|
||||
|
||||
export function getEffectiveThemeStyle(): "light" | "dark" {
|
||||
const themeStyle = getThemeStyle();
|
||||
if (themeStyle === "auto") {
|
||||
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
return themeStyle === "dark" ? "dark" : "light";
|
||||
}
|
||||
@@ -455,9 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
|
||||
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
|
||||
hoistedNoteId?: string;
|
||||
} = {}) {
|
||||
// Dynamic import to avoid import issues in tests.
|
||||
const appContext = (await import("../components/app_context.js")).default;
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
const activeContext = glob.appContext?.tabManager?.getActiveContext();
|
||||
if (!activeContext) {
|
||||
return;
|
||||
}
|
||||
@@ -467,7 +465,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
|
||||
if (!existingSubcontext) {
|
||||
// The target split is not already open, open a new split with it.
|
||||
const { ntxId } = subContexts[subContexts.length - 1];
|
||||
appContext.triggerCommand("openNewNoteSplit", {
|
||||
glob.appContext?.triggerCommand("openNewNoteSplit", {
|
||||
ntxId,
|
||||
notePath: targetNoteId,
|
||||
hoistedNoteId: openOpts.hoistedNoteId,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import utils from "./utils.js";
|
||||
import toastService from "./toast.js";
|
||||
import server from "./server.js";
|
||||
import options from "./options.js";
|
||||
import frocaUpdater from "./froca_updater.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
import bundleService from "./bundle.js";
|
||||
import froca from "./froca.js";
|
||||
import frocaUpdater from "./froca_updater.js";
|
||||
import { t } from "./i18n.js";
|
||||
import options from "./options.js";
|
||||
import server from "./server.js";
|
||||
import toast from "./toast.js";
|
||||
import utils from "./utils.js";
|
||||
|
||||
type MessageHandler = (message: WebSocketMessage) => void;
|
||||
let messageHandlers: MessageHandler[] = [];
|
||||
@@ -126,20 +128,14 @@ async function handleMessage(event: MessageEvent<any>) {
|
||||
} else if (message.type === "frontend-update") {
|
||||
await executeFrontendUpdate(message.data.entityChanges);
|
||||
} else if (message.type === "sync-hash-check-failed") {
|
||||
toastService.showError(t("ws.sync-check-failed"), 60000);
|
||||
toast.showError(t("ws.sync-check-failed"), 60000);
|
||||
} else if (message.type === "consistency-checks-failed") {
|
||||
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
|
||||
toast.showError(t("ws.consistency-checks-failed"), 50 * 60000);
|
||||
} else if (message.type === "api-log-messages") {
|
||||
appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages });
|
||||
} else if (message.type === "toast") {
|
||||
toastService.showMessage(message.message);
|
||||
toast.showMessage(message.message);
|
||||
} else if (message.type === "execute-script") {
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const bundleService = (await import("./bundle.js")).default as any;
|
||||
// TODO: Remove after porting the file
|
||||
// @ts-ignore
|
||||
const froca = (await import("./froca.js")).default as any;
|
||||
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
|
||||
|
||||
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
|
||||
@@ -161,7 +157,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
entityChangeIdReachedListeners.push({
|
||||
desiredEntityChangeId: desiredEntityChangeId,
|
||||
desiredEntityChangeId,
|
||||
resolvePromise: res,
|
||||
start: Date.now()
|
||||
});
|
||||
@@ -205,7 +201,7 @@ async function consumeFrontendUpdateData() {
|
||||
} else {
|
||||
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
|
||||
|
||||
toastService.showError(t("ws.encountered-error", { message: e.message }));
|
||||
toast.showError(t("ws.encountered-error", { message: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class SetupController {
|
||||
}
|
||||
|
||||
private async finish() {
|
||||
const syncServerHost = this.syncServerHostInput.value.trim();
|
||||
const syncServerHost = this.syncServerHostInput.value.trim().replace(/\/+$/, "");
|
||||
const syncProxy = this.syncProxyInput.value.trim();
|
||||
const password = this.passwordInput.value;
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/* Import the light color scheme.
|
||||
* This is the base color scheme, always active and overridden by the dark
|
||||
* color scheme stylesheet when necessary. */
|
||||
@import url(./theme-next-light.css);
|
||||
|
||||
/* Import the dark color scheme when the system preference is set to dark mode */
|
||||
@import url(./theme-next-dark.css) (prefers-color-scheme: dark);
|
||||
|
||||
:root {
|
||||
--theme-style-auto: true;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
/* Import the light color scheme.
|
||||
* This is the base color scheme, always active and overridden by the dark
|
||||
* color scheme stylesheet when necessary. */
|
||||
@import url(./theme-light.css);
|
||||
|
||||
/* Import the dark color scheme when the system preference is set to dark mode */
|
||||
@import url(./theme-dark.css) (prefers-color-scheme: dark);
|
||||
|
||||
:root {
|
||||
--theme-style-auto: true;
|
||||
}
|
||||
@@ -626,7 +626,8 @@
|
||||
"date-and-time": "التاريخ والوقت",
|
||||
"no_backup_yet": "لايوجد نسخة احتياطية لحد الان",
|
||||
"enable_daily_backup": "تمكين النسخ الاحتياطي اليومي",
|
||||
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان"
|
||||
"backup_database_now": "نسخ اختياطي لقاعدة البيانات الان",
|
||||
"download": "تنزيل"
|
||||
},
|
||||
"etapi": {
|
||||
"created": "تم الأنشاء",
|
||||
@@ -1129,9 +1130,7 @@
|
||||
"spellcheck": {
|
||||
"title": "التدقيق الاملائي",
|
||||
"enable": "تفعيل التدقيق الاملائي",
|
||||
"language_code_label": "رمز اللغة او رموز اللغات",
|
||||
"available_language_codes_label": "رموز اللغات المتاحة:",
|
||||
"language_code_placeholder": "على سبيل المثال \"en-US\", \"de-AI\""
|
||||
"language_code_label": "رمز اللغة او رموز اللغات"
|
||||
},
|
||||
"note-map": {
|
||||
"button-link-map": "خريطة الروابط",
|
||||
|
||||
@@ -1437,9 +1437,6 @@
|
||||
"description": "这些选项仅适用于桌面版本,浏览器将使用其原生的拼写检查功能。",
|
||||
"enable": "启用拼写检查",
|
||||
"language_code_label": "语言代码",
|
||||
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "多种语言可以用逗号分隔,例如 \"en-US, de-DE, cs\"。 ",
|
||||
"available_language_codes_label": "可用的语言代码:",
|
||||
"restart-required": "拼写检查选项的更改将在应用重启后生效。"
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1386,9 +1386,6 @@
|
||||
"description": "Diese Optionen gelten nur für Desktop-Builds. Browser verwenden ihre eigene native Rechtschreibprüfung.",
|
||||
"enable": "Aktiviere die Rechtschreibprüfung",
|
||||
"language_code_label": "Sprachcode(s)",
|
||||
"language_code_placeholder": "zum Beispiel \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Mehrere Sprachen können mit einem Komma getrennt werden z.B. \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Verfügbare Sprachcodes:",
|
||||
"restart-required": "Änderungen an den Rechtschreibprüfungsoptionen werden nach dem Neustart der Anwendung wirksam."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -806,7 +806,11 @@
|
||||
"board": "Board",
|
||||
"presentation": "Presentation",
|
||||
"include_archived_notes": "Show archived notes",
|
||||
"hide_child_notes": "Hide child notes in tree"
|
||||
"hide_child_notes": "Hide child notes in tree",
|
||||
"open_all_in_tabs": "Open all",
|
||||
"open_all_in_tabs_tooltip": "Open all results in new tabs",
|
||||
"open_all_confirm": "This will open {{count}} notes in new tabs. Continue?",
|
||||
"open_all_too_many": "Too many results ({{count}}). Maximum is {{max}}."
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@@ -860,7 +864,8 @@
|
||||
"collapse": "Collapse to normal size",
|
||||
"title": "Note Map",
|
||||
"fix-nodes": "Fix nodes",
|
||||
"link-distance": "Link distance"
|
||||
"link-distance": "Link distance",
|
||||
"too-many-notes": "This subtree contains {{count}} notes, which exceeds the limit of {{max}} that can be displayed in the note map."
|
||||
},
|
||||
"note_paths": {
|
||||
"title": "Note Paths",
|
||||
@@ -1074,6 +1079,7 @@
|
||||
"edit_title": "Edit title",
|
||||
"rename_note": "Rename note",
|
||||
"enter_new_title": "Enter new note title:",
|
||||
"rename_relation": "Rename relation",
|
||||
"remove_relation": "Remove relation",
|
||||
"confirm_remove_relation": "Are you sure you want to remove the relation?",
|
||||
"specify_new_relation_name": "Specify new relation name (allowed characters: alphanumeric, colon and underscore):",
|
||||
@@ -1400,7 +1406,8 @@
|
||||
"date-and-time": "Date & time",
|
||||
"path": "Path",
|
||||
"database_backed_up_to": "Database has been backed up to {{backupFilePath}}",
|
||||
"no_backup_yet": "no backup yet"
|
||||
"no_backup_yet": "no backup yet",
|
||||
"download": "Download"
|
||||
},
|
||||
"etapi": {
|
||||
"title": "ETAPI",
|
||||
@@ -1498,18 +1505,21 @@
|
||||
"spellcheck": {
|
||||
"title": "Spell Check",
|
||||
"description": "These options apply only for desktop builds, browsers will use their own native spell check.",
|
||||
"enable": "Enable spellcheck",
|
||||
"language_code_label": "Language code(s)",
|
||||
"language_code_placeholder": "for example \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Multiple languages can be separated by comma, e.g. \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Available language codes:",
|
||||
"restart-required": "Changes to the spell check options will take effect after application restart."
|
||||
"enable": "Check spelling",
|
||||
"language_code_label": "Spell Check Languages",
|
||||
"restart-required": "Changes to the spell check options will take effect after application restart.",
|
||||
"custom_dictionary_title": "Custom Dictionary",
|
||||
"custom_dictionary_description": "Words added to the dictionary are synced across all your devices.",
|
||||
"custom_dictionary_edit": "Custom words",
|
||||
"custom_dictionary_edit_description": "Edit the list of words that should not be flagged by the spell checker. Changes will be visible after a restart.",
|
||||
"custom_dictionary_open": "Edit dictionary",
|
||||
"related_description": "Configure spell check languages and custom dictionary."
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Sync Configuration",
|
||||
"server_address": "Server instance address",
|
||||
"timeout": "Sync timeout",
|
||||
"timeout_unit": "milliseconds",
|
||||
"timeout_description": "How long to wait before giving up on a slow sync connection. Increase if you have an unstable network.",
|
||||
"proxy_label": "Sync proxy server (optional)",
|
||||
"note": "Note",
|
||||
"note_description": "If you leave the proxy setting blank, the system proxy will be used (applies to desktop/electron build only).",
|
||||
@@ -1866,7 +1876,8 @@
|
||||
"theme_none": "No syntax highlighting",
|
||||
"theme_group_light": "Light themes",
|
||||
"theme_group_dark": "Dark themes",
|
||||
"copy_title": "Copy to clipboard"
|
||||
"copy_title": "Copy to clipboard",
|
||||
"click_to_copy": "Click to copy"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formatting"
|
||||
|
||||
@@ -1332,7 +1332,8 @@
|
||||
"date-and-time": "Fecha y hora",
|
||||
"path": "Ruta",
|
||||
"database_backed_up_to": "Se ha realizado una copia de seguridad de la base de datos en {{backupFilePath}}",
|
||||
"no_backup_yet": "no hay copia de seguridad todavía"
|
||||
"no_backup_yet": "no hay copia de seguridad todavía",
|
||||
"download": "Descargar"
|
||||
},
|
||||
"etapi": {
|
||||
"title": "ETAPI",
|
||||
@@ -1432,9 +1433,6 @@
|
||||
"description": "Estas opciones se aplican sólo para compilaciones de escritorio; los navegadores utilizarán su corrector ortográfico nativo.",
|
||||
"enable": "Habilitar corrector ortográfico",
|
||||
"language_code_label": "Código(s) de idioma",
|
||||
"language_code_placeholder": "por ejemplo \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Múltiples idiomas se pueden separar por coma, por ejemplo \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Códigos de idioma disponibles:",
|
||||
"restart-required": "Los cambios en las opciones de corrección ortográfica entrarán en vigor después del reinicio de la aplicación."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -1391,9 +1391,6 @@
|
||||
"description": "Ces options s'appliquent uniquement aux versions de bureau, les navigateurs utiliseront leur propre vérification orthographique native.",
|
||||
"enable": "Activer la vérification orthographique",
|
||||
"language_code_label": "Code(s) de langue",
|
||||
"language_code_placeholder": "par exemple \"fr-FR\", \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Plusieurs langues peuvent être séparées par une virgule, par ex. \"fr-FR, en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Codes de langue disponibles :",
|
||||
"restart-required": "Les modifications apportées aux options de vérification orthographique prendront effet après le redémarrage de l'application."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -1071,7 +1071,8 @@
|
||||
"note_already_in_diagram": "Tabhair faoi deara go bhfuil \"{{title}}\" sa léaráid cheana féin.",
|
||||
"enter_title_of_new_note": "Cuir isteach teideal an nóta nua",
|
||||
"default_new_note_title": "nóta nua",
|
||||
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur"
|
||||
"click_on_canvas_to_place_new_note": "Cliceáil ar chanbhás chun nóta nua a chur",
|
||||
"rename_relation": "Athainmnigh an gaol"
|
||||
},
|
||||
"backend_log": {
|
||||
"refresh": "Athnuachan"
|
||||
@@ -1468,12 +1469,15 @@
|
||||
"spellcheck": {
|
||||
"title": "Seiceáil Litrithe",
|
||||
"description": "Ní bhaineann na roghanna seo ach le leaganacha deisce, úsáidfidh brabhsálaithe a seiceáil litrithe dúchasach féin.",
|
||||
"enable": "Cumasaigh seiceáil litrithe",
|
||||
"language_code_label": "Cód(anna) teanga",
|
||||
"language_code_placeholder": "mar shampla \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Is féidir camóg a úsáid chun teangacha iolracha a dheighilt óna chéile, m.sh. \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Cóid teanga atá ar fáil:",
|
||||
"restart-required": "Tiocfaidh athruithe ar na roghanna seiceála litrithe i bhfeidhm tar éis atosú an fheidhmchláir."
|
||||
"enable": "Seiceáil litriú",
|
||||
"language_code_label": "Seiceáil Litrithe Teangacha",
|
||||
"restart-required": "Tiocfaidh athruithe ar na roghanna seiceála litrithe i bhfeidhm tar éis atosú an fheidhmchláir.",
|
||||
"custom_dictionary_title": "Foclóir Saincheaptha",
|
||||
"custom_dictionary_description": "Déantar focail a chuirtear leis an bhfoclóir a sioncrónú ar fud do ghléasanna go léir.",
|
||||
"custom_dictionary_edit": "Focail saincheaptha",
|
||||
"custom_dictionary_edit_description": "Cuir an liosta focal in eagar nach ceart don seiceálaí litrithe a mharcáil. Beidh athruithe le feiceáil tar éis atosaithe.",
|
||||
"custom_dictionary_open": "Cuir an foclóir in eagar",
|
||||
"related_description": "Cumraigh teangacha seiceála litrithe agus foclóir saincheaptha."
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Cumraíocht Sioncrónaithe",
|
||||
@@ -2294,7 +2298,9 @@
|
||||
"sample_user_journey": "Turas Úsáideora",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Venn",
|
||||
"sample_ishikawa": "Ishikawa"
|
||||
"sample_ishikawa": "Ishikawa",
|
||||
"sample_treeview": "Radharc Crann",
|
||||
"sample_wardley": "Léarscáil Wardley"
|
||||
},
|
||||
"llm_chat": {
|
||||
"placeholder": "Clóscríobh teachtaireacht...",
|
||||
@@ -2404,6 +2410,9 @@
|
||||
"processing": "Ag próiseáil...",
|
||||
"processing_started": "Tá próiseáil OCR tosaithe. Fan nóiméad agus athnuachan le do thoil.",
|
||||
"processing_failed": "Theip ar phróiseáil OCR a thosú",
|
||||
"view_extracted_text": "Féach ar théacs eastósctha (OCR)"
|
||||
"view_extracted_text": "Féach ar théacs eastósctha (OCR)",
|
||||
"processing_complete": "Próiseáil OCR críochnaithe.",
|
||||
"text_filtered_low_confidence": "Bhraith OCR téacs le muinín {{confidence}}%, ach caitheadh leis é mar is é {{threshold}}% an tairseach íosta atá agat.",
|
||||
"open_media_settings": "Oscail Socruithe"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1461,9 +1461,6 @@
|
||||
"description": "ये विकल्प सिर्फ़ डेस्कटॉप वर्जन के लिए हैं, ब्राउज़र अपना स्पेल चेक इस्तेमाल करेंगे।",
|
||||
"enable": "स्पेल चेक चालू करें",
|
||||
"language_code_label": "भाषा कोड (Language code)",
|
||||
"language_code_placeholder": "जैसे \"en-US\", \"hi-IN\"",
|
||||
"multiple_languages_info": "कई भाषाओं को कॉमा से अलग किया जा सकता है, जैसे \"en-US, hi-IN\"। ",
|
||||
"available_language_codes_label": "उपलब्ध भाषा कोड:",
|
||||
"restart-required": "स्पेल चेक में बदलाव ऐप रीस्टार्ट करने के बाद ही दिखेंगे।"
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -538,12 +538,12 @@
|
||||
"new_tab": "Nuova scheda"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Sommario",
|
||||
"table_of_contents": "Tabella dei Contenuti",
|
||||
"options": "Opzioni",
|
||||
"no_headings": "Nessun titolo."
|
||||
},
|
||||
"table_of_contents": {
|
||||
"title": "Sommario",
|
||||
"title": "Tabella dei Contenuti",
|
||||
"description": "L'indice apparirà nelle note di testo quando la nota contiene più di un numero definito di titoli. È possibile personalizzare questo numero:",
|
||||
"unit": "titoli",
|
||||
"disable_info": "È anche possibile utilizzare questa opzione per disattivare efficacemente l'indice impostando un numero molto alto.",
|
||||
@@ -593,7 +593,7 @@
|
||||
"collapseExpand": "collassa/espande il nodo",
|
||||
"notSet": "non impostato",
|
||||
"goBackForwards": "indietro/avanti nella cronologia",
|
||||
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">finestra \"Vai a\"</a>",
|
||||
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Vai a\"</a>",
|
||||
"title": "Scheda riassuntiva",
|
||||
"noteNavigation": "Nota navigazione",
|
||||
"scrollToActiveNote": "scorri fino alla nota attiva",
|
||||
@@ -853,7 +853,7 @@
|
||||
"archived": "Le note con questa etichetta non saranno visibili per impostazione predefinita nei risultati di ricerca (anche nelle finestre di dialogo Vai a, Aggiungi collegamento ecc.).",
|
||||
"run_on_instance": "Definire su quale istanza di Trilium eseguire questa operazione. L'impostazione predefinita è tutte le istanze.",
|
||||
"exclude_from_export": "le note (con la loro sottostruttura) non saranno incluse in nessuna esportazione di note",
|
||||
"run": "definisce su quali eventi deve essere eseguito lo script. I valori possibili sono:\n<ul>\n<li>frontendStartup - quando il frontend Trilium viene avviato (o aggiornato), ma non su dispositivi mobili.</li>\n<li>mobileStartup - quando il frontend Trilium viene avviato (o aggiornato) su dispositivi mobili.</li>\n<li>backendStartup - quando viene avviato il backend Trilium</li>\n<li>hourly - eseguire una volta all'ora. È possibile utilizzare l'etichetta aggiuntiva <code>runAtHour</code> per specificare a che ora.</li>\n<li>daily - eseguire una volta al giorno</li>\n</ul>",
|
||||
"run": "definisce su quali eventi deve essere eseguito lo script. I valori possibili sono:\n<ul>\n<li>frontendStartup - quando il frontend Trilium viene avviato (o aggiornato), ma non su dispositivi mobili.</li>\n<li>mobileStartup - quando il frontend Trilium viene avviato (o aggiornato) su dispositivi mobili.</li>\n<li>backendStartup - quando viene avviato il backend Trilium.</li>\n<li>hourly - eseguire una volta all'ora. È possibile utilizzare l'etichetta aggiuntiva <code>runAtHour</code> per specificare a che ora.</li>\n<li>daily - eseguire una volta al giorno.</li>\n</ul>",
|
||||
"run_at_hour": "A che ora deve essere eseguito. Deve essere utilizzato insieme a <code>#run=hourly</code>. Può essere definito più volte per più esecuzioni durante il giorno.",
|
||||
"disable_inclusion": "gli script con questa etichetta non saranno inclusi nell'esecuzione dello script principale.",
|
||||
"sorted": "mantiene le note figlie ordinate alfabeticamente per titolo",
|
||||
@@ -1100,7 +1100,7 @@
|
||||
"show_help": "Mostra aiuto",
|
||||
"about": "Informazioni su Trilium Notes",
|
||||
"logout": "Esci",
|
||||
"show-cheatsheet": "Mostra il foglietto illustrativo",
|
||||
"show-cheatsheet": "Mostra la scheda riassuntiva",
|
||||
"toggle-zen-mode": "Modalità Zen",
|
||||
"new-version-available": "Nuovo aggiornamento disponibile",
|
||||
"download-update": "Ottieni la versione {{latestVersion}}",
|
||||
@@ -1149,7 +1149,8 @@
|
||||
"export_as_image": "Esporta come immagine",
|
||||
"export_as_image_png": "PNG (raster)",
|
||||
"export_as_image_svg": "SVG (vector)",
|
||||
"note_map": "Mappa"
|
||||
"note_map": "Mappa",
|
||||
"view_ocr_text": "Visualizza il testo OCR"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Il widget pulsante '{{componentId}}' non ha un gestore di clic definito"
|
||||
@@ -1439,7 +1440,8 @@
|
||||
"note_already_in_diagram": "Nota che \"{{title}}\" è già presente nel diagramma.",
|
||||
"enter_title_of_new_note": "Inserisci il titolo della nuova nota",
|
||||
"default_new_note_title": "nuova nota",
|
||||
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota"
|
||||
"click_on_canvas_to_place_new_note": "Clicca sulla tela per inserire una nuova nota",
|
||||
"rename_relation": "Rinomina relazione"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Pulizia del database",
|
||||
@@ -1541,12 +1543,28 @@
|
||||
},
|
||||
"images": {
|
||||
"images_section_title": "Immagini",
|
||||
"download_images_automatically": "Scarica automaticamente le immagini per l'utilizzo offline.",
|
||||
"download_images_description": "L'HTML incollato può contenere riferimenti a immagini online; Trilium troverà tali riferimenti e scaricherà le immagini in modo che siano disponibili offline.",
|
||||
"enable_image_compression": "Abilita la compressione delle immagini",
|
||||
"max_image_dimensions": "Larghezza/altezza massima di un'immagine (l'immagine verrà ridimensionata se supera questa impostazione).",
|
||||
"download_images_automatically": "Scarica automaticamente le immagini",
|
||||
"download_images_description": "Scarica le immagini online a cui si fa riferimento nel codice HTML incollato, in modo che siano disponibili offline.",
|
||||
"enable_image_compression": "Compressione delle immagini",
|
||||
"max_image_dimensions": "Dimensioni massime dell'immagine",
|
||||
"max_image_dimensions_unit": "pixel",
|
||||
"jpeg_quality_description": "Qualità JPEG (10 - qualità peggiore, 100 - qualità migliore, 50 - 85 è consigliato)"
|
||||
"jpeg_quality_description": "Il range consigliato è compreso tra 50 e 85. Valori più bassi riducono le dimensioni del file, mentre valori più alti preservano i dettagli.",
|
||||
"enable_image_compression_description": "Comprimi e ridimensiona le immagini al momento del caricamento o dell'inserimento.",
|
||||
"max_image_dimensions_description": "Le immagini che superano queste dimensioni verranno ridimensionate automaticamente.",
|
||||
"jpeg_quality": "Qualità JPEG",
|
||||
"ocr_section_title": "Estrazione di testo (OCR)",
|
||||
"ocr_related_content_languages": "Lingue dei contenuti (utilizzate per l'estrazione del testo)",
|
||||
"ocr_auto_process": "Elaborazione automatica dei nuovi file",
|
||||
"ocr_auto_process_description": "Estrai automaticamente il testo dai file appena caricati o incollati.",
|
||||
"ocr_min_confidence": "Livello minimo di confidenza",
|
||||
"ocr_confidence_description": "Estrai solo il testo che supera questa soglia di affidabilità. Valori inferiori includono più testo, ma potrebbero risultare meno accurati.",
|
||||
"batch_ocr_title": "Elabora i file esistenti",
|
||||
"batch_ocr_description": "Estrai il testo da tutte le immagini, i PDF e i documenti Office presenti nei tuoi appunti. L'operazione potrebbe richiedere un po' di tempo a seconda del numero di file.",
|
||||
"batch_ocr_start": "Avvia l'elaborazione in batch",
|
||||
"batch_ocr_starting": "Avvio dell'elaborazione in batch...",
|
||||
"batch_ocr_progress": "Elaborazione di {{processed}} su {{total}} file...",
|
||||
"batch_ocr_completed": "Elaborazione in batch completata! Sono stati elaborati {{processed}} file.",
|
||||
"batch_ocr_error": "Errore durante l'elaborazione in batch: {{error}}"
|
||||
},
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "Timeout cancellazione allegato",
|
||||
@@ -1654,12 +1672,15 @@
|
||||
"spellcheck": {
|
||||
"title": "Controllo ortografico",
|
||||
"description": "Queste opzioni sono valide solo per le versioni desktop; i browser utilizzeranno il proprio controllo ortografico nativo.",
|
||||
"enable": "Abilita il controllo ortografico",
|
||||
"language_code_label": "Codice/i della lingua",
|
||||
"language_code_placeholder": "ad esempio \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "È possibile separare più lingue con una virgola, ad esempio \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Codici lingua disponibili:",
|
||||
"restart-required": "Le modifiche alle opzioni di controllo ortografico avranno effetto dopo il riavvio dell'applicazione."
|
||||
"enable": "Controlla l'ortografia",
|
||||
"language_code_label": "Lingue del controllo ortografico",
|
||||
"restart-required": "Le modifiche alle opzioni di controllo ortografico avranno effetto dopo il riavvio dell'applicazione.",
|
||||
"custom_dictionary_title": "Dizionario personalizzato",
|
||||
"custom_dictionary_description": "Le parole aggiunte al dizionario vengono sincronizzate su tutti i tuoi dispositivi.",
|
||||
"custom_dictionary_edit": "Parole personalizzate",
|
||||
"custom_dictionary_edit_description": "Modifica l'elenco delle parole che non devono essere segnalate dal correttore ortografico. Le modifiche saranno visibili dopo il riavvio.",
|
||||
"custom_dictionary_open": "Modifica il dizionario",
|
||||
"related_description": "Configura le lingue del controllo ortografico e il dizionario personalizzato."
|
||||
},
|
||||
"api_log": {
|
||||
"close": "Vicino"
|
||||
@@ -1940,7 +1961,7 @@
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Lingue dei contenuti",
|
||||
"description": "Seleziona una o più lingue che desideri visualizzare nella sezione \"Proprietà di base\" di una nota di testo di sola lettura o modificabile. Ciò consentirà funzionalità come il controllo ortografico o il supporto per la scrittura da destra a sinistra."
|
||||
"description": "Seleziona una o più lingue che devono comparire nell'elenco di selezione delle lingue nella sezione \"Proprietà di base\" di una nota di testo in sola lettura o modificabile. Ciò consentirà di utilizzare funzioni quali il controllo ortografico, il supporto per la scrittura da destra a sinistra e l'estrazione del testo (OCR)."
|
||||
},
|
||||
"switch_layout_button": {
|
||||
"title_vertical": "Sposta il riquadro di modifica in basso",
|
||||
@@ -2247,7 +2268,9 @@
|
||||
"sample_user_journey": "Percorso dell'utente",
|
||||
"sample_xy": "XY",
|
||||
"sample_venn": "Venn",
|
||||
"sample_ishikawa": "Ishikawa"
|
||||
"sample_ishikawa": "Ishikawa",
|
||||
"sample_treeview": "TreeView",
|
||||
"sample_wardley": "Mappa di Wardley"
|
||||
},
|
||||
"llm_chat": {
|
||||
"placeholder": "Scrivi un messaggio...",
|
||||
@@ -2278,7 +2301,8 @@
|
||||
"note_context_enabled": "Clicca qui per disattivare il contesto della nota: {{title}}",
|
||||
"note_context_disabled": "Clicca per includere la nota corrente nel contesto",
|
||||
"no_provider_message": "Non è stato configurato alcun fornitore di IA. Aggiungine uno per iniziare a chattare.",
|
||||
"add_provider": "Aggiungi un fornitore di IA"
|
||||
"add_provider": "Aggiungi un fornitore di IA",
|
||||
"sources_summary": "{{count}} fonti provenienti da {{sites}} siti"
|
||||
},
|
||||
"sidebar_chat": {
|
||||
"title": "Chat AI",
|
||||
@@ -2304,6 +2328,61 @@
|
||||
"delete_provider_confirmation": "Sei sicuro di voler eliminare il provider \"{{name}}\"?",
|
||||
"api_key": "Chiave API",
|
||||
"api_key_placeholder": "Inserisci la tua chiave API",
|
||||
"cancel": "Annulla"
|
||||
"cancel": "Annulla",
|
||||
"feature_not_enabled": "Abilita la funzione sperimentale LLM in Impostazioni → Avanzate → Funzioni sperimentali per utilizzare le integrazioni basate sull'intelligenza artificiale.",
|
||||
"mcp_title": "MCP (Model Context Protocol)",
|
||||
"mcp_enabled": "Server MCP",
|
||||
"mcp_enabled_description": "Rendi pubblico un endpoint MCP (Model Context Protocol) in modo che gli assistenti di programmazione basati sull'intelligenza artificiale (ad esempio Claude Code, GitHub Copilot) possano leggere e modificare le tue note. L'endpoint è accessibile solo da localhost.",
|
||||
"mcp_endpoint_title": "URL dell'endpoint",
|
||||
"mcp_endpoint_description": "Aggiungi questo URL alla configurazione MCP del tuo assistente AI",
|
||||
"tools": {
|
||||
"search_notes": "Cerca nelle note",
|
||||
"get_note": "Prendi nota",
|
||||
"get_note_content": "Visualizza il contenuto della nota",
|
||||
"update_note_content": "Aggiorna il contenuto della nota",
|
||||
"append_to_note": "Aggiungi alla nota",
|
||||
"create_note": "Crea nota",
|
||||
"get_attributes": "Recupera gli attributi",
|
||||
"get_attribute": "Ottieni attributo",
|
||||
"set_attribute": "Imposta attributo",
|
||||
"delete_attribute": "Elimina attributo",
|
||||
"get_child_notes": "Recupera le note relative ai figli",
|
||||
"get_subtree": "Ottieni sottostruttura",
|
||||
"load_skill": "Carica skill",
|
||||
"web_search": "Ricerca sul web",
|
||||
"note_in_parent": "<Note/> in <Parent/>",
|
||||
"get_attachment": "Scarica l'allegato",
|
||||
"get_attachment_content": "Leggi il contenuto dell'allegato"
|
||||
}
|
||||
},
|
||||
"ocr": {
|
||||
"extracted_text": "Testo estratto (OCR)",
|
||||
"extracted_text_title": "Testo estratto (OCR)",
|
||||
"loading_text": "Caricamento del testo OCR in corso...",
|
||||
"no_text_available": "Non è disponibile alcun testo OCR",
|
||||
"no_text_explanation": "Questo documento non è stato sottoposto a elaborazione OCR per l'estrazione del testo oppure non è stato trovato alcun testo.",
|
||||
"failed_to_load": "Impossibile caricare il testo OCR",
|
||||
"process_now": "Elaborazione OCR",
|
||||
"processing": "Elaborazione in corso...",
|
||||
"processing_started": "L'elaborazione OCR è stata avviata. Attendere qualche istante e aggiorna.",
|
||||
"processing_complete": "Elaborazione OCR completata.",
|
||||
"processing_failed": "Impossibile avviare l'elaborazione OCR",
|
||||
"text_filtered_low_confidence": "L'OCR ha rilevato il testo con un livello di affidabilità del {{confidence}}%, ma è stato scartato perché la soglia minima impostata è del {{threshold}}%.",
|
||||
"open_media_settings": "Apri Impostazioni",
|
||||
"view_extracted_text": "Visualizza il testo estratto (OCR)"
|
||||
},
|
||||
"mind-map": {
|
||||
"addChild": "Aggiungi figlio",
|
||||
"addParent": "Aggiungi genitore",
|
||||
"addSibling": "Aggiungi un fratello o una sorella",
|
||||
"removeNode": "Rimuovi nodo",
|
||||
"focus": "Modalità Focus",
|
||||
"cancelFocus": "Annulla modalità Focus",
|
||||
"moveUp": "Sposta su",
|
||||
"moveDown": "Sposta giù",
|
||||
"link": "Collegamento",
|
||||
"linkBidirectional": "Collegamento bidirezionale",
|
||||
"clickTips": "Clicca sul nodo di destinazione",
|
||||
"summary": "Sommario"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1027,12 +1027,15 @@
|
||||
"spellcheck": {
|
||||
"title": "スペルチェック",
|
||||
"description": "これらのオプションはデスクトップビルドにのみ適用され、ブラウザはそれぞれのネイティブスペルチェックを使用します。",
|
||||
"enable": "スペルチェックを有効",
|
||||
"language_code_label": "言語コード",
|
||||
"language_code_placeholder": "例えば \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "複数の言語はカンマで区切ることができます。例: \"en-US, de-DE, cs\"。 ",
|
||||
"available_language_codes_label": "使用可能な言語コード:",
|
||||
"restart-required": "スペルチェックオプションの変更は、アプリケーションの再起動後に有効になります。"
|
||||
"enable": "スペルチェック",
|
||||
"language_code_label": "スペルチェック対応言語",
|
||||
"restart-required": "スペルチェックオプションの変更は、アプリケーションの再起動後に有効になります。",
|
||||
"custom_dictionary_title": "カスタム辞書",
|
||||
"custom_dictionary_description": "辞書に追加した単語は、すべてのデバイス間で同期されます。",
|
||||
"custom_dictionary_edit": "カスタム単語",
|
||||
"custom_dictionary_edit_description": "スペルチェッカーでエラーとして検出されないようにする単語リストを編集します。変更は再起動後に反映されます。",
|
||||
"custom_dictionary_open": "辞書の編集",
|
||||
"related_description": "スペルチェック対応言語とカスタム辞書を設定します。"
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "同期設定",
|
||||
@@ -1570,7 +1573,8 @@
|
||||
"click_on_canvas_to_place_new_note": "キャンバスをクリックして新しいノートを配置",
|
||||
"connection_exists": "これらのノート間の接続 '{{name}}' は既に存在します。",
|
||||
"start_dragging_relations": "ここからリレーションをドラッグして、別のノートにドロップします。",
|
||||
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。"
|
||||
"note_already_in_diagram": "ノート「{{title}}」はすでに図に含まれています。",
|
||||
"rename_relation": "リレーション名の変更"
|
||||
},
|
||||
"database_anonymization": {
|
||||
"title": "データベースの匿名化",
|
||||
@@ -2234,7 +2238,9 @@
|
||||
"sample_user_journey": "ユーザージャーニー図",
|
||||
"sample_xy": "XY チャート",
|
||||
"sample_venn": "ベン図",
|
||||
"sample_ishikawa": "石川図"
|
||||
"sample_ishikawa": "石川図",
|
||||
"sample_treeview": "ツリービュー",
|
||||
"sample_wardley": "ウォードリーマップ"
|
||||
},
|
||||
"llm_chat": {
|
||||
"placeholder": "メッセージを入力してください…",
|
||||
@@ -2344,6 +2350,9 @@
|
||||
"processing": "処理中…",
|
||||
"processing_started": "OCR 処理が開始されました。しばらくお待ちいただき、ページを更新してください。",
|
||||
"processing_failed": "OCR 処理の開始に失敗しました",
|
||||
"view_extracted_text": "抽出されたテキスト(OCR)を表示"
|
||||
"view_extracted_text": "抽出されたテキスト(OCR)を表示",
|
||||
"processing_complete": "OCR 処理が完了しました。",
|
||||
"text_filtered_low_confidence": "OCR は {{confidence}}% の信頼度でテキストを検出しましたが、最小しきい値が {{threshold}}% であるため、破棄されました。",
|
||||
"open_media_settings": "設定を開く"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Custom script laden mislukt",
|
||||
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
|
||||
"message": "Script voor de notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
|
||||
},
|
||||
"scripting-error": "Error met script: {{title}}",
|
||||
"widget-list-error": {
|
||||
|
||||
@@ -1665,9 +1665,6 @@
|
||||
"description": "Te opcje dotyczą tylko wersji desktopowych, przeglądarki będą używać własnego natywnego sprawdzania pisowni.",
|
||||
"enable": "Włącz sprawdzanie pisowni",
|
||||
"language_code_label": "Kod(y) języka",
|
||||
"language_code_placeholder": "na przykład \"pl-PL\", \"en-US\"",
|
||||
"multiple_languages_info": "Wiele języków można oddzielić przecinkiem, np. \"en-US, de-DE, pl\". ",
|
||||
"available_language_codes_label": "Dostępne kody języków:",
|
||||
"restart-required": "Zmiany w opcjach sprawdzania pisowni wejdą w życie po ponownym uruchomieniu aplikacji."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -1435,9 +1435,6 @@
|
||||
"description": "Estas opções aplicam-se apenas às versões desktop; os navegadores usarão a sua própria verificação ortográfica nativa.",
|
||||
"enable": "Ativar verificação ortográfica",
|
||||
"language_code_label": "Código(s) de idioma",
|
||||
"language_code_placeholder": "por exemplo \"en-US\", \"de-AT\", \"pt-BR\"",
|
||||
"multiple_languages_info": "Múltiplos idiomas podem ser separados por vírgula, por exemplo: \"en-US, de-DE, pt-BR, cs\". ",
|
||||
"available_language_codes_label": "Códigos de idioma disponíveis:",
|
||||
"restart-required": "As alterações nas opções de verificação ortográfica terão efeito após reiniciar a aplicação."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -1944,9 +1944,6 @@
|
||||
"description": "Estas opções se aplicam apenas às versões desktop; os navegadores usarão sua própria verificação ortográfica nativa.",
|
||||
"enable": "Habilitar verificação ortográfica",
|
||||
"language_code_label": "Código(s) de idioma",
|
||||
"language_code_placeholder": "por exemplo \"en-US\", \"de-AT\", \"pt-BR\"",
|
||||
"multiple_languages_info": "Múltiplos idiomas podem ser separados por vírgula, por exemplo: \"en-US, de-DE, pt-BR, cs\". ",
|
||||
"available_language_codes_label": "Códigos de idioma disponíveis:",
|
||||
"restart-required": "As alterações nas opções de verificação ortográfica terão efeito após reiniciar o aplicativo."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
@@ -1237,12 +1237,9 @@
|
||||
"title": "titlu"
|
||||
},
|
||||
"spellcheck": {
|
||||
"available_language_codes_label": "Coduri de limbă disponibile:",
|
||||
"description": "Aceste opțiuni se aplică doar pentru aplicația de desktop, navigatoarele web folosesc propriile corectoare ortografice.",
|
||||
"enable": "Activează corectorul ortografic",
|
||||
"language_code_label": "Codurile de limbă",
|
||||
"language_code_placeholder": "de exemplu „en-US”, „de-AT”",
|
||||
"multiple_languages_info": "Mai multe limbi pot fi separate prin virgulă, e.g. \"en-US, de-DE, cs\". ",
|
||||
"title": "Corector ortografic",
|
||||
"restart-required": "Schimbările asupra setărilor corectorului ortografic vor fi aplicate după restartarea aplicației."
|
||||
},
|
||||
|
||||
@@ -1679,10 +1679,7 @@
|
||||
"title": "Проверка орфографии",
|
||||
"enable": "Включить проверку орфографии",
|
||||
"language_code_label": "Код(ы) языков",
|
||||
"multiple_languages_info": "Несколько языков можно разделять запятой, например, \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Доступные коды языков:",
|
||||
"restart-required": "Изменения параметров проверки орфографии вступят в силу после перезапуска приложения.",
|
||||
"language_code_placeholder": "например \"en-US\", \"de-AT\"",
|
||||
"description": "Эти параметры применимы только для десктопных сборок, браузеры будут использовать собственную встроенную проверку орфографии."
|
||||
},
|
||||
"attribute_editor": {
|
||||
|
||||
@@ -23,10 +23,33 @@
|
||||
"close": "Kapat",
|
||||
"delete_notes_preview": "Not önizlemesini sil",
|
||||
"delete_all_clones_description": "Tüm klonları da sil (son değişikliklerden geri alınabilir)",
|
||||
"erase_notes_description": "Normal (yazılımsal) silme işlemi, notları yalnızca silinmiş olarak işaretler ve belirli bir süre içinde (son değişiklikler iletişim kutusunda) geri alınabilir. Bu seçeneği işaretlemek, notları hemen siler ve notların geri alınması mümkün olmaz."
|
||||
"erase_notes_description": "Normal (yazılımsal) silme işlemi, notları yalnızca silinmiş olarak işaretler ve belirli bir süre içinde (son değişiklikler iletişim kutusunda) geri alınabilir. Bu seçeneği işaretlemek, notları hemen siler ve notların geri alınması mümkün olmaz.",
|
||||
"erase_notes_warning": "Notları, tüm kopyaları da dahil olmak üzere kalıcı olarak silin (geri alınamaz). Bu işlem, uygulamanın yeniden yüklenmesine neden olacaktır.",
|
||||
"notes_to_be_deleted": "Aşağıdaki notlar silinecektir. ({{notesCount}})",
|
||||
"no_note_to_delete": "Hiçbir not silinmeyecek (sadece kopyaları silinecek).",
|
||||
"broken_relations_to_be_deleted": "Aşağıdaki ilişkiler koparılacak ve silinecektir ({{ relationCount}})",
|
||||
"cancel": "İptal",
|
||||
"ok": "Tamam",
|
||||
"deleted_relation_text": "{{- note}} (silinecek) notu, {{- source}} kaynağından kaynaklanan {{- relation}} ilişkisi tarafından referans alınmaktadır."
|
||||
},
|
||||
"export": {
|
||||
"close": "Kapat"
|
||||
"close": "Kapat",
|
||||
"export_note_title": "Notu dışa aktar",
|
||||
"export_type_subtree": "Bu not ve tüm torunları",
|
||||
"format_html": "HTML - tüm biçimlendirmeyi koruduğu için önerilir",
|
||||
"format_html_zip": "ZIP arşivindeki HTML dosyaları - tüm biçimlendirmeyi koruduğu için bu yöntem önerilir.",
|
||||
"format_markdown": "Markdown - bu, biçimlendirmenin büyük kısmını korur.",
|
||||
"format_opml": "OPML - yalnızca metin için anahat değişim biçimi. Biçimlendirme, resimler ve dosyalar dahil edilmez.",
|
||||
"opml_version_1": "OPML v1.0 - yalnızca düz metin",
|
||||
"opml_version_2": "OPML v2.0 - HTML de destekler",
|
||||
"export_type_single": "Yalnızca bu not, alt öğeleri olmadan",
|
||||
"export": "Dışa aktar",
|
||||
"choose_export_type": "Lütfen önce dışa aktarma türünü seçin",
|
||||
"export_status": "Dışa aktarma durumu",
|
||||
"export_in_progress": "Dışa aktarma devam ediyor: {{progressCount}}",
|
||||
"export_finished_successfully": "Dışa aktarma başarıyla tamamlandı.",
|
||||
"format_pdf": "PDF - yazdırma veya paylaşım amaçları için.",
|
||||
"share-format": "Web yayını için HTML - paylaşılan notlarda kullanılan temayı kullanır, ancak statik bir web sitesi olarak yayınlanabilir."
|
||||
},
|
||||
"import": {
|
||||
"chooseImportFile": "İçe aktarım dosyası",
|
||||
@@ -58,7 +81,9 @@
|
||||
"widget-render-error": {
|
||||
"title": "Özel React widget'ı çizilirken sorun yaşandı"
|
||||
},
|
||||
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}"
|
||||
"scripting-error": "Kullanıcı tanımlı betik hatası: {{title}}",
|
||||
"widget-missing-parent": "Özel widget'ın zorunlu '{{property}}' özelliği tanımlanmamıştır.\n\nBu komut dosyasının bir kullanıcı arayüzü öğesi olmadan çalıştırılması gerekiyorsa, bunun yerine '#run=frontendStartup' kullanın.",
|
||||
"open-script-note": "Komut dosyası notunu aç"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Bağlantı ekle",
|
||||
@@ -103,5 +128,32 @@
|
||||
"are_you_sure_remove_note": "\"{{title}}\" notunu ilişki haritasından kaldırmak istediğinize emin misiniz?. ",
|
||||
"also_delete_note": "Notu da sil",
|
||||
"if_you_dont_check": "Bunu işaretlemezseniz, not yalnızca ilişki haritasından kaldırılacaktır."
|
||||
},
|
||||
"help": {
|
||||
"title": "Özet tablo",
|
||||
"editShortcuts": "Klavye kısayollarını düzenle",
|
||||
"noteNavigation": "Not içinde gezinme",
|
||||
"goUpDown": "Notlar listesinde yukarı/aşağı gitmek",
|
||||
"collapseExpand": "düğümü daralt/genişlet",
|
||||
"notSet": "ayarlanmamış",
|
||||
"goBackForwards": "tarihte geri/ileri git",
|
||||
"showJumpToNoteDialog": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Şuraya Git\" iletişim kutusunu göster</a>",
|
||||
"scrollToActiveNote": "Aktif nota kaydır",
|
||||
"jumpToParentNote": "Üst nota git",
|
||||
"collapseWholeTree": "Tüm not ağacını daralt",
|
||||
"collapseSubTree": "Alt ağacı daralt",
|
||||
"tabShortcuts": "Sekme kısayolları",
|
||||
"newTabNoteLink": "Not bağlantısı notu yeni sekmede açılır",
|
||||
"newTabWithActivationNoteLink": "Not bağlantısına tıklandığında not yeni bir sekmede açılır ve etkinleştirilir",
|
||||
"onlyInDesktop": "Yalnızca masaüstünde (Electron derlemesi)",
|
||||
"openEmptyTab": "boş sekmeyi aç",
|
||||
"closeActiveTab": "aktif sekmeyi kapat",
|
||||
"activateNextTab": "sonraki sekmeyi etkinleştir",
|
||||
"activatePreviousTab": "önceki sekmeyi etkinleştir",
|
||||
"creatingNotes": "Not oluşturma",
|
||||
"createNoteAfter": "etkin nottan sonra yeni not oluşturma",
|
||||
"createNoteInto": "aktif nota yeni bir alt not oluşturun",
|
||||
"editBranchPrefix": "<a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/tree-concepts.html#prefix\">prefix</a> değerini aktif not klonunun düzenle",
|
||||
"movingCloningNotes": "Notları taşıma / klonlama"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@
|
||||
"calendar_root": "標記應用作為每日筆記的根。只應標記一個筆記。",
|
||||
"archived": "含有此標籤的筆記預設在搜尋結果中不可見(也適用於跳轉至、新增連結對話方塊等)。",
|
||||
"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": "定義腳本應運行的事件。可能的值包括:\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": "含有此標籤的腳本不會包含在父腳本執行中。",
|
||||
@@ -706,7 +706,8 @@
|
||||
"export_as_image": "匯出為圖片",
|
||||
"export_as_image_png": "PNG (點陣)",
|
||||
"export_as_image_svg": "SVG (向量)",
|
||||
"note_map": "筆記地圖"
|
||||
"note_map": "筆記地圖",
|
||||
"view_ocr_text": "顯示 OCR 文字"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
|
||||
@@ -1196,12 +1197,28 @@
|
||||
},
|
||||
"images": {
|
||||
"images_section_title": "圖片",
|
||||
"download_images_automatically": "自動下載圖片以供離線使用。",
|
||||
"download_images_description": "貼上的 HTML 可能包含線上圖片的引用,Trilium 會找到這些引用並下載圖片,以便它們可以離線使用。",
|
||||
"enable_image_compression": "啟用圖片壓縮",
|
||||
"max_image_dimensions": "圖片的最大寬度 / 高度(超過此限制的圖片將會被縮放)。",
|
||||
"jpeg_quality_description": "JPEG 質量(10 - 最差質量,100 最佳質量,建議為 50 - 85)",
|
||||
"max_image_dimensions_unit": "像素"
|
||||
"download_images_automatically": "自動下載圖片",
|
||||
"download_images_description": "從貼上的 HTML 下載引用的線上圖片以便離線使用。",
|
||||
"enable_image_compression": "圖片壓縮",
|
||||
"max_image_dimensions": "最大圖片尺寸",
|
||||
"jpeg_quality_description": "建議範圍為 50–85。較低的數值可縮小檔案大小,較高的數值則能保留更多細節。",
|
||||
"max_image_dimensions_unit": "像素",
|
||||
"enable_image_compression_description": "在上傳或貼上圖片時壓縮並調整圖片大小。",
|
||||
"max_image_dimensions_description": "超過此尺寸的圖片將會自動調整大小。",
|
||||
"jpeg_quality": "JPEG 品質",
|
||||
"ocr_section_title": "文字擷取(OCR)",
|
||||
"ocr_related_content_languages": "內容語言(用於文字擷取)",
|
||||
"ocr_auto_process": "自動處理新檔案",
|
||||
"ocr_auto_process_description": "自動從新上傳或貼上的檔案中擷取文字。",
|
||||
"ocr_min_confidence": "最低信賴度",
|
||||
"ocr_confidence_description": "僅提取高於此信賴度閾值的文字。較低的閾值雖能包含更多文字,但準確度可能較低。",
|
||||
"batch_ocr_title": "處理現有檔案",
|
||||
"batch_ocr_description": "從筆記中的所有現有圖片、PDF 檔案及 Office 文件中擷取文字。根據檔案數量多寡,此過程可能需要一些時間。",
|
||||
"batch_ocr_start": "開始批次處理",
|
||||
"batch_ocr_starting": "開始批次處理…",
|
||||
"batch_ocr_progress": "正在處理 {{processed}} 個檔案,共 {{total}} 個檔案…",
|
||||
"batch_ocr_completed": "批次處理完成!已處理 {{processed}} 個檔案。",
|
||||
"batch_ocr_error": "批次處理期間發生錯誤:{{error}}"
|
||||
},
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "附件清理超時",
|
||||
@@ -1381,9 +1398,6 @@
|
||||
"description": "這些選項僅適用於桌面版,瀏覽器將使用其原生的拼寫檢查功能。",
|
||||
"enable": "啟用拼寫檢查",
|
||||
"language_code_label": "語言代碼",
|
||||
"language_code_placeholder": "例如 \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "多種語言可以用逗號分隔,例如 \"en-US, de-DE, cs\"。 ",
|
||||
"available_language_codes_label": "可用的語言代碼:",
|
||||
"restart-required": "拼寫檢查選項的更改將在應用重啟後生效。"
|
||||
},
|
||||
"sync_2": {
|
||||
@@ -1497,7 +1511,8 @@
|
||||
"new-feature": "新增",
|
||||
"collections": "集合",
|
||||
"ai-chat": "AI 聊天",
|
||||
"spreadsheet": "試算表"
|
||||
"spreadsheet": "試算表",
|
||||
"llm-chat": "AI 對話"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "保護筆記",
|
||||
@@ -1866,7 +1881,7 @@
|
||||
},
|
||||
"content_language": {
|
||||
"title": "內文語言",
|
||||
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查或從右向左之類的功能。"
|
||||
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查、從右向左及文字擷取 (OCR) 等功能。"
|
||||
},
|
||||
"switch_layout_button": {
|
||||
"title_vertical": "將編輯面板移至底部",
|
||||
@@ -2046,7 +2061,9 @@
|
||||
"title": "實驗性選項",
|
||||
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
|
||||
"new_layout_name": "新版面配置",
|
||||
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
|
||||
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。",
|
||||
"llm_name": "AI / LLM 對話",
|
||||
"llm_description": "啟用由大語言模型驅動的 AI 聊天側邊欄及 LLM 聊天筆記。"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "與伺服器通訊錯誤",
|
||||
@@ -2229,6 +2246,121 @@
|
||||
"sample_user_journey": "使用者旅程",
|
||||
"sample_xy": "XY 圖表",
|
||||
"sample_venn": "韋恩圖",
|
||||
"sample_ishikawa": "魚骨圖"
|
||||
"sample_ishikawa": "魚骨圖",
|
||||
"sample_treeview": "樹狀視圖",
|
||||
"sample_wardley": "沃德利地圖"
|
||||
},
|
||||
"llm_chat": {
|
||||
"placeholder": "輸入訊息…",
|
||||
"send": "送出",
|
||||
"sending": "正在送出…",
|
||||
"empty_state": "請在下方輸入訊息,開啟對話。",
|
||||
"searching_web": "正在搜尋網頁…",
|
||||
"web_search": "網頁搜尋",
|
||||
"note_tools": "筆記存取",
|
||||
"sources": "來源",
|
||||
"sources_summary": "來自 {{sites}} 個網站的 {{count}} 個來源",
|
||||
"extended_thinking": "延伸思考",
|
||||
"legacy_models": "傳統模型",
|
||||
"thinking": "正在思考…",
|
||||
"thought_process": "思考過程",
|
||||
"tool_calls": "{{count}} 次工具調用",
|
||||
"input": "輸入",
|
||||
"result": "結果",
|
||||
"error": "錯誤",
|
||||
"tool_error": "失敗",
|
||||
"total_tokens": "{{total}} 個詞元",
|
||||
"tokens_detail": "{{prompt}} 提示詞 + {{completion}} 補全",
|
||||
"tokens_used": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
|
||||
"tokens_used_with_cost": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}})",
|
||||
"tokens_used_with_model": "{{model}}:{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
|
||||
"tokens_used_with_model_and_cost": "{{model}}:{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}})",
|
||||
"tokens": "詞元",
|
||||
"context_used": "已使用 {{percentage}}%",
|
||||
"note_context_enabled": "點擊以禁用筆記上下文:{{title}}",
|
||||
"note_context_disabled": "點擊將當前筆記納入上下文",
|
||||
"no_provider_message": "尚未設定任何 AI 服務提供者。請新增一個以開始聊天。",
|
||||
"add_provider": "新增 AI 提供者"
|
||||
},
|
||||
"ocr": {
|
||||
"processing_complete": "OCR 處理已完成。",
|
||||
"processing_failed": "無法啟動 OCR 處理",
|
||||
"text_filtered_low_confidence": "OCR 偵測到的信賴度為 {{confidence}}%,但因您的最低閾值設定為 {{threshold}}%,故該結果已被捨棄。",
|
||||
"open_media_settings": "開啟設定",
|
||||
"view_extracted_text": "檢視擷取的文字 (OCR)",
|
||||
"extracted_text": "已擷取的文字 (OCR)",
|
||||
"extracted_text_title": "已擷取的文字 (OCR)",
|
||||
"loading_text": "正在載入 OCR 文字…",
|
||||
"no_text_available": "無 OCR 文字可用",
|
||||
"no_text_explanation": "此筆記尚未經過 OCR 文字擷取處理,或未找到任何文字。",
|
||||
"failed_to_load": "載入 OCR 文字失敗",
|
||||
"process_now": "處理 OCR",
|
||||
"processing": "正在處理…",
|
||||
"processing_started": "OCR 處理已開始。請稍候片刻並重新整理頁面。"
|
||||
},
|
||||
"mind-map": {
|
||||
"addChild": "新增子節點",
|
||||
"addParent": "新增父節點",
|
||||
"addSibling": "新增同級節點",
|
||||
"removeNode": "刪除節點",
|
||||
"focus": "專注模式",
|
||||
"cancelFocus": "退出專注模式",
|
||||
"moveUp": "上移",
|
||||
"moveDown": "下移",
|
||||
"link": "連結",
|
||||
"linkBidirectional": "雙向連結",
|
||||
"clickTips": "請點擊目標節點",
|
||||
"summary": "摘要"
|
||||
},
|
||||
"llm": {
|
||||
"settings_title": "AI / LLM",
|
||||
"settings_description": "設定 AI 及大型語言模型整合。",
|
||||
"feature_not_enabled": "請前往「設定」→「進階」→「實驗性功能」啟用 LLM 實驗性功能,即可使用 AI 整合。",
|
||||
"add_provider": "新增提供者",
|
||||
"add_provider_title": "新增 AI 提供者",
|
||||
"configured_providers": "已設定的提供者",
|
||||
"no_providers_configured": "尚未設定任何提供者。",
|
||||
"provider_name": "名稱",
|
||||
"provider_type": "提供者",
|
||||
"actions": "動作",
|
||||
"delete_provider": "刪除",
|
||||
"delete_provider_confirmation": "您確定要刪除提供者 \"{{name}}\" 嗎?",
|
||||
"api_key": "API 金鑰",
|
||||
"api_key_placeholder": "請輸入您的 API 金鑰",
|
||||
"cancel": "取消",
|
||||
"mcp_title": "MCP(模型上下文協定)",
|
||||
"mcp_enabled": "MCP 伺服器",
|
||||
"mcp_enabled_description": "公開一個模型上下文協定 (MCP) 端點,以便人工智慧編程助手(例如 Claude Code、GitHub Copilot)能夠讀取並修改您的筆記。此端點僅限從 localhost 存取。",
|
||||
"mcp_endpoint_title": "端點網址",
|
||||
"mcp_endpoint_description": "將此網址新增至您的 AI 助理的 MCP 設定中",
|
||||
"tools": {
|
||||
"search_notes": "搜尋筆記",
|
||||
"get_note": "取得筆記",
|
||||
"get_note_content": "取得筆記內容",
|
||||
"update_note_content": "更新筆記內容",
|
||||
"append_to_note": "追加至筆記",
|
||||
"create_note": "建立筆記",
|
||||
"get_attributes": "取得屬性",
|
||||
"get_attribute": "取得屬性",
|
||||
"set_attribute": "設定屬性",
|
||||
"delete_attribute": "移除屬性",
|
||||
"get_child_notes": "取得子筆記",
|
||||
"get_subtree": "取得子階層",
|
||||
"load_skill": "載入技能",
|
||||
"web_search": "網頁搜尋",
|
||||
"note_in_parent": "<Note/> 於 <Parent/>",
|
||||
"get_attachment": "取得附件",
|
||||
"get_attachment_content": "讀取附件內容"
|
||||
}
|
||||
},
|
||||
"sidebar_chat": {
|
||||
"title": "AI 對話",
|
||||
"launcher_title": "打開 AI 對話",
|
||||
"new_chat": "開始新對話",
|
||||
"save_chat": "將對話保存至筆記",
|
||||
"empty_state": "開始會話",
|
||||
"history": "對話歷史",
|
||||
"recent_chats": "最近的對話",
|
||||
"no_chats": "無先前的對話記錄"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1744,9 +1744,6 @@
|
||||
"description": "Ці параметри застосовуються лише для збірок для ПК, браузери використовуватимуть власну вбудовану перевірку орфографії.",
|
||||
"enable": "Увімкнути перевірку орфографії",
|
||||
"language_code_label": "Код(и) мови",
|
||||
"language_code_placeholder": "наприклад, \"en-US\", \"de-AT\"",
|
||||
"multiple_languages_info": "Кілька мов можна розділяти комами, наприклад, \"en-US, de-DE, cs\". ",
|
||||
"available_language_codes_label": "Доступні коди мови:",
|
||||
"restart-required": "Зміни в параметрах перевірки орфографії набудуть чинності після перезапуску програми."
|
||||
},
|
||||
"sync_2": {
|
||||
|
||||
1
apps/client/src/types-lib.d.ts
vendored
1
apps/client/src/types-lib.d.ts
vendored
@@ -66,6 +66,7 @@ declare module "preact" {
|
||||
interface ElectronWebViewElement extends JSX.HTMLAttributes<HTMLElement> {
|
||||
src: string;
|
||||
class: string;
|
||||
key?: string | number;
|
||||
}
|
||||
|
||||
interface IntrinsicElements {
|
||||
|
||||
6
apps/client/src/types.d.ts
vendored
6
apps/client/src/types.d.ts
vendored
@@ -24,6 +24,7 @@ interface CustomGlobals {
|
||||
getReferenceLinkTitle: (href: string) => Promise<string>;
|
||||
getReferenceLinkTitleSync: (href: string) => string;
|
||||
getActiveContextNote: () => FNote | null;
|
||||
getThemeStyle: () => "auto" | "light" | "dark";
|
||||
ESLINT: Library;
|
||||
appContext: AppContext;
|
||||
froca: Froca;
|
||||
@@ -51,8 +52,9 @@ interface CustomGlobals {
|
||||
isElectron: boolean;
|
||||
isRtl: boolean;
|
||||
iconRegistry: IconRegistry;
|
||||
themeCssUrl: string;
|
||||
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
|
||||
theme: string;
|
||||
themeBase?: "next" | "next-light" | "next-dark";
|
||||
customThemeCssUrl?: string;
|
||||
iconPackCss: string;
|
||||
headingStyle: "plain" | "underline" | "markdown";
|
||||
layoutOrientation: "vertical" | "horizontal";
|
||||
|
||||
@@ -87,7 +87,7 @@ function buildUserAttribute(attr: AttributeWithDefinitions): ComponentChildren {
|
||||
content = <><Icon icon={value === "true" ? "bx bx-check-square" : "bx bx-square"} />{" "}<strong>{attr.friendlyName}</strong></>;
|
||||
break;
|
||||
case "url":
|
||||
content = <a href={value} target="_blank" rel="noopener noreferrer">{attr.friendlyName}</a>;
|
||||
content = <a href={value} target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()}>{attr.friendlyName}</a>;
|
||||
break;
|
||||
case "color":
|
||||
style = { backgroundColor: value, color: getReadableTextColor(value) };
|
||||
|
||||
@@ -180,11 +180,13 @@ export function useNoteIds(note: FNote | null | undefined, viewType: ViewTypeOpt
|
||||
|
||||
// Refresh on alterations to the note subtree.
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (note && loadResults.getBranchRows().some(branch =>
|
||||
branch.parentNoteId === note.noteId
|
||||
|| noteIds.includes(branch.parentNoteId ?? ""))
|
||||
if (note && (
|
||||
loadResults.getNoteReorderings().includes(note.noteId)
|
||||
|| loadResults.getBranchRows().some(branch =>
|
||||
branch.parentNoteId === note.noteId
|
||||
|| noteIds.includes(branch.parentNoteId ?? ""))
|
||||
|| loadResults.getAttributeRows().some(attr => attr.name === "archived" && attr.noteId && noteIds.includes(attr.noteId))
|
||||
) {
|
||||
)) {
|
||||
refreshNoteIds();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
|
||||
|
||||
|
||||
if (dateNote.hasChildren()) {
|
||||
const childNoteIds = await dateNote.getSubtreeNoteIds();
|
||||
const childNoteIds = dateNote.getChildNoteIds();
|
||||
for (const childNoteId of childNoteIds) {
|
||||
childNoteToDateMapping[childNoteId] = startDate;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
:root {
|
||||
/* Default values to be overridden by themes */
|
||||
--calendar-coll-event-background-lightness: 95%;
|
||||
--calendar-coll-event-background-saturation: 80%;
|
||||
--calendar-coll-event-background-color: var(--accented-background-color);
|
||||
--calendar-coll-event-text-color: var(--main-text-color);
|
||||
--calendar-coll-event-hover-filter: none;
|
||||
--callendar-coll-event-archived-sripe-color: #00000013;
|
||||
--calendar-coll-today-background-color: var(--more-accented-background-color);
|
||||
}
|
||||
|
||||
.calendar-view {
|
||||
--fc-event-border-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-bg-color: var(--calendar-coll-event-background-color);
|
||||
--fc-event-text-color: var(--calendar-coll-event-text-color);
|
||||
--fc-event-border-color: var(--calendar-coll-event-text-color, var(--main-text-color));
|
||||
--fc-event-bg-color: var(--calendar-coll-event-background-color, var(--accented-background-color));
|
||||
--fc-event-text-color: var(--calendar-coll-event-text-color, var(--main-text-color));
|
||||
--fc-event-selected-overlay-color: transparent;
|
||||
--fc-today-bg-color: var(--calendar-coll-today-background-color);
|
||||
--fc-today-bg-color: var(--calendar-coll-today-background-color, var(--more-accented-background-color));
|
||||
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@@ -123,7 +112,7 @@
|
||||
z-index: -1;
|
||||
|
||||
--c1: transparent;
|
||||
--c2: var(--callendar-coll-event-archived-sripe-color);
|
||||
--c2: var(--callendar-coll-event-archived-sripe-color, #00000013);
|
||||
|
||||
background: repeating-linear-gradient(45deg, var(--c1), var(--c1) 8px,
|
||||
var(--c2) 8px, var(--c2) 16px);
|
||||
@@ -153,8 +142,8 @@
|
||||
--fc-event-text-color: var(--custom-color);
|
||||
|
||||
--fc-event-bg-color: hsl(var(--custom-color-hue),
|
||||
var(--calendar-coll-event-background-saturation),
|
||||
var(--calendar-coll-event-background-lightness)) !important;
|
||||
var(--calendar-coll-event-background-saturation, 80%),
|
||||
var(--calendar-coll-event-background-lightness, 95%)) !important;
|
||||
}
|
||||
|
||||
.calendar-view a.fc-timegrid-event:focus-visible,
|
||||
@@ -171,7 +160,7 @@
|
||||
|
||||
.calendar-view a.fc-timegrid-event:hover,
|
||||
.calendar-view a.fc-daygrid-event:hover {
|
||||
filter: var(--calendar-coll-event-hover-filter);
|
||||
filter: var(--calendar-coll-event-hover-filter, none);
|
||||
border-color: var(--fc-event-text-color);
|
||||
text-decoration: none;
|
||||
color: currentColor;
|
||||
|
||||
@@ -82,6 +82,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
|
||||
hi: () => import("@fullcalendar/core/locales/hi"),
|
||||
ga: null,
|
||||
cn: () => import("@fullcalendar/core/locales/zh-cn"),
|
||||
cs: () => import("@fullcalendar/core/locales/cs"),
|
||||
tw: () => import("@fullcalendar/core/locales/zh-tw"),
|
||||
ro: () => import("@fullcalendar/core/locales/ro"),
|
||||
ru: () => import("@fullcalendar/core/locales/ru"),
|
||||
@@ -143,7 +144,12 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
||||
const event = api.getEventById(noteId);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
if (!event || !note) continue;
|
||||
event.setProp("title", note.title);
|
||||
// Only update the title if it has actually changed.
|
||||
// setProp() triggers FullCalendar's eventChange callback, which would
|
||||
// re-save the event's dates and cause unwanted side effects.
|
||||
if (event.title !== note.title) {
|
||||
event.setProp("title", note.title);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -298,6 +304,12 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean, c
|
||||
}, [ note, componentId ]);
|
||||
|
||||
const onEventChange = useCallback(async (e: EventChangeArg) => {
|
||||
// Only process actual date/time changes, not other property changes (e.g., title via setProp).
|
||||
const datesChanged = e.oldEvent.start?.getTime() !== e.event.start?.getTime()
|
||||
|| e.oldEvent.end?.getTime() !== e.event.end?.getTime()
|
||||
|| e.oldEvent.allDay !== e.event.allDay;
|
||||
if (!datesChanged) return;
|
||||
|
||||
const { startDate, endDate } = parseStartEndDateFromEvent(e.event);
|
||||
if (!startDate) return;
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ export default function useRowTableEditing(api: RefObject<Tabulator>, attributeD
|
||||
if (type === "labels") {
|
||||
if (typeof newValue === "boolean") {
|
||||
newValue = newValue ? "true" : "false";
|
||||
} else if (typeof newValue === "number") {
|
||||
newValue = String(newValue);
|
||||
}
|
||||
setLabel(noteId, name, newValue);
|
||||
} else if (type === "relations") {
|
||||
|
||||
@@ -80,9 +80,19 @@ export default function JumpToNoteDialogComponent() {
|
||||
break;
|
||||
}
|
||||
|
||||
$autoComplete
|
||||
.trigger("focus")
|
||||
.trigger("select");
|
||||
$autoComplete.trigger("focus");
|
||||
|
||||
if (mode === "commands") {
|
||||
// In command mode, place caret at end instead of selecting all text
|
||||
// This preserves the ">" prefix when the user starts typing
|
||||
const input = autocompleteRef.current;
|
||||
if (input) {
|
||||
const len = input.value.length;
|
||||
input.setSelectionRange(len, len);
|
||||
}
|
||||
} else {
|
||||
$autoComplete.trigger("select");
|
||||
}
|
||||
|
||||
// Add keyboard shortcut for full search
|
||||
shortcutService.bindElShortcut($autoComplete, "ctrl+return", () => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import appContext, { type EventData } from "../components/app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import katex from "../services/math.js";
|
||||
import options from "../services/options.js";
|
||||
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
||||
import RightPanelWidget from "./right_panel_widget.js";
|
||||
@@ -125,77 +124,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
this.triggerCommand("reEvaluateRightPaneVisibility");
|
||||
}
|
||||
|
||||
extractOuterTag(htmlStr: string | null) {
|
||||
if (htmlStr === null) {
|
||||
return null;
|
||||
}
|
||||
// Regular expressions that match only the outermost tag
|
||||
const regex = /^<([a-zA-Z]+)([^>]*)>/;
|
||||
const match = htmlStr.match(regex);
|
||||
if (match) {
|
||||
const tagName = match[1].toLowerCase(); // Extract tag name
|
||||
const attributes = match[2].trim(); // Extract label attributes
|
||||
return { tagName, attributes };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
areOuterTagsConsistent(str1: string | null, str2: string | null) {
|
||||
const tag1 = this.extractOuterTag(str1);
|
||||
const tag2 = this.extractOuterTag(str2);
|
||||
// If one of them has no label, returns false
|
||||
if (!tag1 || !tag2) {
|
||||
return false;
|
||||
}
|
||||
// Compare tag names and attributes to see if they are the same
|
||||
return tag1.tagName === tag2.tagName && tag1.attributes === tag2.attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering formulas in strings using katex
|
||||
*
|
||||
* @param html Note's html content
|
||||
* @returns The HTML content with mathematical formulas rendered by KaTeX.
|
||||
*/
|
||||
async replaceMathTextWithKatax(html: string) {
|
||||
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
|
||||
const matches = [...html.matchAll(mathTextRegex)];
|
||||
let modifiedText = html;
|
||||
|
||||
if (matches.length > 0) {
|
||||
// Process all matches asynchronously
|
||||
for (const match of matches) {
|
||||
const latexCode = match[1];
|
||||
let rendered;
|
||||
|
||||
try {
|
||||
rendered = katex.renderToString(latexCode, {
|
||||
throwOnError: false
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof ReferenceError && e.message.includes("katex is not defined")) {
|
||||
// Load KaTeX if it is not already loaded
|
||||
try {
|
||||
rendered = katex.renderToString(latexCode, {
|
||||
throwOnError: false
|
||||
});
|
||||
} catch (renderError) {
|
||||
console.error("KaTeX rendering error after loading library:", renderError);
|
||||
rendered = match[0]; // Fall back to original if error persists
|
||||
}
|
||||
} else {
|
||||
console.error("KaTeX rendering error:", e);
|
||||
rendered = match[0]; // Fall back to original on error
|
||||
}
|
||||
}
|
||||
|
||||
// Replace the matched formula in the modified text
|
||||
modifiedText = modifiedText.replace(match[0], rendered);
|
||||
}
|
||||
}
|
||||
return modifiedText;
|
||||
}
|
||||
|
||||
async getHighlightList(content: string, optionsHighlightsList: string[]) {
|
||||
// matches a span containing background-color
|
||||
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
|
||||
@@ -239,9 +167,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
const $highlightsList = $("<ol>");
|
||||
let prevEndIndex = -1,
|
||||
hlLiCount = 0;
|
||||
let prevSubHtml: string | null = null;
|
||||
// Used to determine if a string is only a formula
|
||||
const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/;
|
||||
|
||||
for (let match: RegExpMatchArray | null = null, hltIndex = 0; (match = combinedRegex.exec(content)) !== null; hltIndex++) {
|
||||
const subHtml = match[0];
|
||||
@@ -257,25 +182,14 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
// If the previous element is connected to this element in HTML, then concatenate them into one.
|
||||
$highlightsList.children().last().append(subHtml);
|
||||
} else {
|
||||
// TODO: can't be done with $(subHtml).text()?
|
||||
//Can’t remember why regular expressions are used here, but modified to $(subHtml).text() works as expected
|
||||
//const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
|
||||
const hasText = $(subHtml).text().trim();
|
||||
|
||||
if (hasText) {
|
||||
const substring = content.substring(prevEndIndex, startIndex);
|
||||
//If the two elements have the same style and there are only formulas in between, append the formulas and the current element to the end of the previous element.
|
||||
if (this.areOuterTagsConsistent(prevSubHtml, subHtml) && onlyMathRegex.test(substring)) {
|
||||
const $lastLi = $highlightsList.children("li").last();
|
||||
$lastLi.append(await this.replaceMathTextWithKatax(substring));
|
||||
$lastLi.append(subHtml);
|
||||
} else {
|
||||
$highlightsList.append(
|
||||
$("<li>")
|
||||
.html(subHtml)
|
||||
.on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex))
|
||||
);
|
||||
}
|
||||
$highlightsList.append(
|
||||
$("<li>")
|
||||
.html(subHtml)
|
||||
.on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex))
|
||||
);
|
||||
|
||||
hlLiCount++;
|
||||
} else {
|
||||
@@ -284,7 +198,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
}
|
||||
}
|
||||
prevEndIndex = endIndex;
|
||||
prevSubHtml = subHtml;
|
||||
}
|
||||
return {
|
||||
$highlightsList,
|
||||
|
||||
@@ -2,10 +2,13 @@ import "./CollectionProperties.css";
|
||||
|
||||
import { t } from "i18next";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { useRef } from "preact/hooks";
|
||||
import { useRef, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import appContext from "../../components/app_context";
|
||||
import dialogService from "../../services/dialog";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||
import { useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||
@@ -24,6 +27,8 @@ export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
presentation: "bx bx-rectangle"
|
||||
};
|
||||
|
||||
const MAX_OPEN_TABS = 50;
|
||||
|
||||
export default function CollectionProperties({ note, centerChildren, rightChildren }: {
|
||||
note: FNote;
|
||||
centerChildren?: ComponentChildren;
|
||||
@@ -31,6 +36,7 @@ export default function CollectionProperties({ note, centerChildren, rightChildr
|
||||
}) {
|
||||
const [ viewType, setViewType ] = useViewType(note);
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const [ isOpening, setIsOpening ] = useState(false);
|
||||
|
||||
return ([ "book", "search" ].includes(noteType ?? "") &&
|
||||
<div className="collection-properties">
|
||||
@@ -43,11 +49,59 @@ export default function CollectionProperties({ note, centerChildren, rightChildr
|
||||
</div>
|
||||
<div className="right-container">
|
||||
{rightChildren}
|
||||
{noteType === "search" && (
|
||||
<OpenAllButton note={note} isOpening={isOpening} setIsOpening={setIsOpening} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenAllButton({ note, isOpening, setIsOpening }: {
|
||||
note: FNote;
|
||||
isOpening: boolean;
|
||||
setIsOpening: (value: boolean) => void;
|
||||
}) {
|
||||
const noteIds = note.getChildNoteIds();
|
||||
const count = noteIds.length;
|
||||
|
||||
const handleOpenAll = async () => {
|
||||
if (count === 0) return;
|
||||
|
||||
if (count > MAX_OPEN_TABS) {
|
||||
await dialogService.info(t("book_properties.open_all_too_many", { count, max: MAX_OPEN_TABS }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (count > 10) {
|
||||
const confirmed = await dialogService.confirm(t("book_properties.open_all_confirm", { count }));
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
setIsOpening(true);
|
||||
try {
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const noteId = noteIds[i];
|
||||
const isLast = i === noteIds.length - 1;
|
||||
await appContext.tabManager.openTabWithNoteWithHoisting(noteId, {
|
||||
activate: isLast
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsOpening(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon={isOpening ? "bx bx-loader-alt bx-spin" : "bx bx-window-open"}
|
||||
text={t("book_properties.open_all_in_tabs_tooltip")}
|
||||
onClick={handleOpenAll}
|
||||
disabled={count === 0 || isOpening}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewTypeSwitcher({ viewType, setViewType }: { viewType: ViewTypeOptions, setViewType: (newValue: ViewTypeOptions) => void }) {
|
||||
// Keyboard shortcut
|
||||
const dropdownContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -42,8 +42,11 @@ export default function NoteIcon() {
|
||||
setIcon(note?.getIcon());
|
||||
}, [ note, iconClass, workspaceIconClass ]);
|
||||
|
||||
const isDisabled = viewScope?.viewMode !== "default"
|
||||
|| note?.isMetadataReadOnly;
|
||||
|
||||
if (isMobile()) {
|
||||
return <MobileNoteIconSwitcher note={note} icon={icon} />;
|
||||
return <MobileNoteIconSwitcher note={note} icon={icon} disabled={isDisabled} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -55,16 +58,17 @@ export default function NoteIcon() {
|
||||
dropdownOptions={{ autoClose: "outside" }}
|
||||
buttonClassName={`note-icon tn-focusable-button ${icon ?? "bx bx-empty"}`}
|
||||
hideToggleArrow
|
||||
disabled={viewScope?.viewMode !== "default"}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{ note && <NoteIconList note={note} onHide={() => dropdownRef?.current?.hide()} columnCount={12} /> }
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNoteIconSwitcher({ note, icon }: {
|
||||
function MobileNoteIconSwitcher({ note, icon, disabled }: {
|
||||
note: FNote | null | undefined;
|
||||
icon: string | null | undefined;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const [ modalShown, setModalShown ] = useState(false);
|
||||
const { windowWidth } = useWindowSize();
|
||||
@@ -76,6 +80,7 @@ function MobileNoteIconSwitcher({ note, icon }: {
|
||||
icon={icon ?? "bx bx-empty"}
|
||||
text={t("note_icon.change_note_icon")}
|
||||
onClick={() => setModalShown(true)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{createPortal((
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.note-detail-note-map {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -54,4 +54,4 @@
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
/* End of styling the slider */
|
||||
/* End of styling the slider */
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import "./NoteMap.css";
|
||||
import { getThemeStyle, MapType, NoteMapWidgetMode, rgb2hex } from "./utils";
|
||||
import { RefObject } from "preact";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { useElementSize, useNoteLabel } from "../react/hooks";
|
||||
|
||||
import ForceGraph from "force-graph";
|
||||
import { RefObject } from "preact";
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import hoisted_note from "../../services/hoisted_note";
|
||||
import { t } from "../../services/i18n";
|
||||
import { getEffectiveThemeStyle } from "../../services/theme";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useElementSize, useNoteLabel } from "../react/hooks";
|
||||
import NoItems from "../react/NoItems";
|
||||
import Slider from "../react/Slider";
|
||||
import { loadNotesAndRelations, NoteMapLinkObject, NoteMapNodeObject, NotesAndRelationsData } from "./data";
|
||||
import { CssData, setupRendering } from "./rendering";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { t } from "../../services/i18n";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import appContext from "../../components/app_context";
|
||||
import Slider from "../react/Slider";
|
||||
import hoisted_note from "../../services/hoisted_note";
|
||||
import { MapType, NoteMapWidgetMode, rgb2hex } from "./utils";
|
||||
|
||||
/** Maximum number of notes to render in the note map before showing a warning. */
|
||||
const MAX_NOTES_THRESHOLD = 1_000;
|
||||
|
||||
interface NoteMapProps {
|
||||
note: FNote;
|
||||
@@ -31,6 +38,7 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
|
||||
const containerSize = useElementSize(parentRef);
|
||||
const [ fixNodes, setFixNodes ] = useState(false);
|
||||
const [ linkDistance, setLinkDistance ] = useState(40);
|
||||
const [ tooManyNotes, setTooManyNotes ] = useState<number | null>(null);
|
||||
const notesAndRelationsRef = useRef<NotesAndRelationsData>();
|
||||
|
||||
const mapRootId = useMemo(() => {
|
||||
@@ -40,9 +48,9 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
|
||||
return hoisted_note.getHoistedNoteId();
|
||||
} else if (mapRootIdLabel) {
|
||||
return mapRootIdLabel;
|
||||
} else {
|
||||
return appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
|
||||
}
|
||||
return appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
|
||||
|
||||
}, [ note ]);
|
||||
|
||||
// Build the note graph instance.
|
||||
@@ -58,6 +66,14 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
|
||||
const includeRelations = labelValues("mapIncludeRelation");
|
||||
loadNotesAndRelations(mapRootId, excludeRelations, includeRelations, mapType).then((notesAndRelations) => {
|
||||
if (!containerRef.current || !styleResolverRef.current) return;
|
||||
|
||||
// Guard against rendering too many notes which would freeze the browser.
|
||||
if (notesAndRelations.nodes.length > MAX_NOTES_THRESHOLD) {
|
||||
setTooManyNotes(notesAndRelations.nodes.length);
|
||||
return;
|
||||
}
|
||||
setTooManyNotes(null);
|
||||
|
||||
const cssData = getCssData(containerRef.current, styleResolverRef.current);
|
||||
|
||||
// Configure rendering properties.
|
||||
@@ -67,7 +83,7 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
|
||||
noteIdToSizeMap: notesAndRelations.noteIdToSizeMap,
|
||||
cssData,
|
||||
notesAndRelations,
|
||||
themeStyle: getThemeStyle(),
|
||||
themeStyle: getEffectiveThemeStyle(),
|
||||
widgetMode,
|
||||
mapType
|
||||
});
|
||||
@@ -113,9 +129,15 @@ export default function NoteMap({ note, widgetMode, parentRef }: NoteMapProps) {
|
||||
node.fx = undefined;
|
||||
node.fy = undefined;
|
||||
}
|
||||
})
|
||||
});
|
||||
}, [ fixNodes, mapType ]);
|
||||
|
||||
if (tooManyNotes) {
|
||||
return (
|
||||
<NoItems icon="bx bx-error-circle" text={t("note_map.too-many-notes", { count: tooManyNotes, max: MAX_NOTES_THRESHOLD })} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="note-map-widget">
|
||||
<div className="btn-group btn-group-sm map-type-switcher content-floating-buttons top-left" role="group">
|
||||
@@ -159,7 +181,7 @@ function MapTypeSwitcher({ icon, text, type, currentMapType, setMapType }: {
|
||||
onClick={() => setMapType(type)}
|
||||
frame
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData {
|
||||
@@ -170,5 +192,5 @@ function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData
|
||||
fontFamily: containerStyle.fontFamily,
|
||||
textColor: rgb2hex(containerStyle.color),
|
||||
mutedTextColor: rgb2hex(styleResolverStyle.color)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,3 @@ export function generateColorFromString(str: string, themeStyle: "light" | "dark
|
||||
return color;
|
||||
}
|
||||
|
||||
export function getThemeStyle() {
|
||||
const documentStyle = window.getComputedStyle(document.documentElement);
|
||||
return documentStyle.getPropertyValue("--theme-style")?.trim() as "light" | "dark";
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import FormTextBox from "./react/FormTextBox";
|
||||
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import server from "../services/server";
|
||||
import "./note_title.css";
|
||||
import { isLaunchBarConfig } from "../services/utils";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import appContext from "../components/app_context";
|
||||
import branches from "../services/branches";
|
||||
import { t } from "../services/i18n";
|
||||
import protected_session_holder from "../services/protected_session_holder";
|
||||
import server from "../services/server";
|
||||
import { isIMEComposing } from "../services/shortcuts";
|
||||
import clsx from "clsx";
|
||||
import FormTextBox from "./react/FormTextBox";
|
||||
import { useNoteContext, useNoteProperty, useSpacedUpdate, useTriliumEvent, useTriliumEvents } from "./react/hooks";
|
||||
|
||||
export default function NoteTitleWidget(props: {className?: string}) {
|
||||
const { note, noteId, componentId, viewScope, noteContext, parentComponent } = useNoteContext();
|
||||
@@ -25,8 +26,7 @@ export default function NoteTitleWidget(props: {className?: string}) {
|
||||
const isReadOnly = note === null
|
||||
|| note === undefined
|
||||
|| (note.isProtected && !protected_session_holder.isProtectedSessionAvailable())
|
||||
|| isLaunchBarConfig(note.noteId)
|
||||
|| note.noteId.startsWith("_help_")
|
||||
|| note.isMetadataReadOnly
|
||||
|| viewScope?.viewMode !== "default";
|
||||
setReadOnly(isReadOnly);
|
||||
}, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]);
|
||||
@@ -58,11 +58,29 @@ export default function NoteTitleWidget(props: {className?: string}) {
|
||||
// Manage focus.
|
||||
const textBoxRef = useRef<HTMLInputElement>(null);
|
||||
const isNewNote = useRef<boolean>();
|
||||
const pendingSelect = useRef<boolean>(false);
|
||||
|
||||
// Re-apply selection when title changes if we have a pending select.
|
||||
// This handles the case where the server sends back entity changes after we've
|
||||
// already called select(), which causes the controlled input to re-render and lose selection.
|
||||
useEffect(() => {
|
||||
if (pendingSelect.current && textBoxRef.current && document.activeElement === textBoxRef.current) {
|
||||
textBoxRef.current.select();
|
||||
pendingSelect.current = false;
|
||||
}
|
||||
}, [title]);
|
||||
|
||||
useTriliumEvents([ "focusOnTitle", "focusAndSelectTitle" ], (e, eventName) => {
|
||||
if (noteContext?.isActive() && textBoxRef.current) {
|
||||
// In the new layout, there are two NoteTitleWidget instances. Only handle if visible.
|
||||
if (!textBoxRef.current.checkVisibility({ checkOpacity: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
textBoxRef.current.focus();
|
||||
if (eventName === "focusAndSelectTitle") {
|
||||
textBoxRef.current.select();
|
||||
pendingSelect.current = true;
|
||||
}
|
||||
isNewNote.current = ("isNewNote" in e ? e.isNewNote : false);
|
||||
}
|
||||
@@ -83,6 +101,9 @@ export default function NoteTitleWidget(props: {className?: string}) {
|
||||
spacedUpdate.scheduleUpdate();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// User started typing, stop re-applying selection
|
||||
pendingSelect.current = false;
|
||||
|
||||
// Skip processing if IME is composing to prevent interference
|
||||
// with text input in CJK languages
|
||||
if (isIMEComposing(e)) {
|
||||
@@ -101,6 +122,7 @@ export default function NoteTitleWidget(props: {className?: string}) {
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
pendingSelect.current = false;
|
||||
spacedUpdate.updateNowIfNecessary();
|
||||
isNewNote.current = false;
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import linkService from "../services/link.js";
|
||||
import froca from "../services/froca.js";
|
||||
import utils, { handleRightToLeftPlacement } from "../services/utils.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import { Dropdown, Tooltip } from "bootstrap";
|
||||
|
||||
import appContext from "../components/app_context.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import linkService from "../services/link.js";
|
||||
import server from "../services/server.js";
|
||||
import shortcutService, { isIMEComposing } from "../services/shortcuts.js";
|
||||
import utils, { handleRightToLeftPlacement } from "../services/utils.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="quick-search input-group input-group-sm">
|
||||
<style>
|
||||
@@ -245,7 +246,7 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
const { searchResultNoteIds, searchResults, error } = await server.get<QuickSearchResponse>(`quick-search/${encodeURIComponent(searchString)}`);
|
||||
|
||||
if (error) {
|
||||
let tooltip = new Tooltip(this.$searchString[0], {
|
||||
const tooltip = new Tooltip(this.$searchString[0], {
|
||||
trigger: "manual",
|
||||
title: `Search error: ${error}`,
|
||||
placement: handleRightToLeftPlacement("right")
|
||||
@@ -289,10 +290,9 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
const resultsToDisplay = this.allSearchResults.slice(startIndex, endIndex);
|
||||
|
||||
for (const result of resultsToDisplay) {
|
||||
const noteId = result.notePath.split("/").pop();
|
||||
if (!noteId) continue;
|
||||
if (!result.notePath) continue;
|
||||
|
||||
const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');
|
||||
const $item = $(`<a class="dropdown-item" tabindex="0" href="#${result.notePath}">`);
|
||||
|
||||
// Build the display HTML with content snippet below the title
|
||||
let itemHtml = `<div class="quick-search-item">
|
||||
@@ -317,23 +317,13 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
|
||||
$item.html(itemHtml);
|
||||
|
||||
$item.on("click", (e) => {
|
||||
$item.on("click auxclick", () => {
|
||||
this.dropdown.hide();
|
||||
e.preventDefault();
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(noteId);
|
||||
}
|
||||
});
|
||||
|
||||
shortcutService.bindElShortcut($item, "return", () => {
|
||||
this.dropdown.hide();
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(noteId);
|
||||
}
|
||||
$item[0].click();
|
||||
});
|
||||
|
||||
this.$dropdownMenu.append($item);
|
||||
@@ -350,24 +340,18 @@ export default class QuickSearchWidget extends BasicWidget {
|
||||
const $link = await linkService.createLink(note.noteId, { showNotePath: true, showNoteIcon: true });
|
||||
$link.addClass("dropdown-item");
|
||||
$link.attr("tabIndex", "0");
|
||||
$link.on("click", (e) => {
|
||||
$link.on("click auxclick", (e) => {
|
||||
this.dropdown.hide();
|
||||
|
||||
if (!e.target || e.target.nodeName !== "A") {
|
||||
// click on the link is handled by link handling, but we want the whole item clickable
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(note.noteId);
|
||||
}
|
||||
if (!e.target || (e.target as HTMLElement).nodeName !== "A") {
|
||||
// click on the <a> is handled by the global goToLink handler,
|
||||
// but we want the whole item clickable
|
||||
$link.find("a")[0]?.dispatchEvent(new MouseEvent(e.type, e.originalEvent as MouseEventInit));
|
||||
}
|
||||
});
|
||||
shortcutService.bindElShortcut($link, "return", () => {
|
||||
this.dropdown.hide();
|
||||
|
||||
const activeContext = appContext.tabManager.getActiveContext();
|
||||
if (activeContext) {
|
||||
activeContext.setNote(note.noteId);
|
||||
}
|
||||
$link.find("a")[0]?.click();
|
||||
});
|
||||
|
||||
this.$dropdownMenu.append($link);
|
||||
|
||||
@@ -825,13 +825,43 @@ export function useWindowSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/twbs/bootstrap/issues/37474
|
||||
// Bootstrap's dispose() sets ALL properties to null. But pending animation callbacks
|
||||
// (scheduled via setTimeout) can still fire and crash when accessing null properties.
|
||||
// We patch dispose() to set safe placeholder values instead of null.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const TooltipProto = Tooltip.prototype as any;
|
||||
const originalDispose = TooltipProto.dispose;
|
||||
const disposedTooltipPlaceholder = {
|
||||
activeTrigger: {},
|
||||
element: document.createElement("noscript")
|
||||
};
|
||||
TooltipProto.dispose = function () {
|
||||
originalDispose.call(this);
|
||||
// After disposal, set safe values so pending callbacks don't crash
|
||||
this._activeTrigger = disposedTooltipPlaceholder.activeTrigger;
|
||||
this._element = disposedTooltipPlaceholder.element;
|
||||
};
|
||||
|
||||
export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Tooltip.Options>) {
|
||||
useEffect(() => {
|
||||
if (!elRef?.current) return;
|
||||
|
||||
const $el = $(elRef.current);
|
||||
$el.tooltip("dispose");
|
||||
const element = elRef.current;
|
||||
const $el = $(element);
|
||||
|
||||
// Dispose any existing tooltip before creating a new one
|
||||
Tooltip.getInstance(element)?.dispose();
|
||||
$el.tooltip(config);
|
||||
|
||||
// Capture the tooltip instance now, since elRef.current may be null during cleanup.
|
||||
const tooltip = Tooltip.getInstance(element);
|
||||
|
||||
return () => {
|
||||
if (element.isConnected) {
|
||||
tooltip?.dispose();
|
||||
}
|
||||
};
|
||||
}, [ elRef, config ]);
|
||||
|
||||
const showTooltip = useCallback(() => {
|
||||
@@ -866,8 +896,14 @@ export function useStaticTooltip(elRef: RefObject<Element>, config?: Partial<Too
|
||||
const hasTooltip = config?.title || elRef.current?.getAttribute("title");
|
||||
if (!elRef?.current || !hasTooltip) return;
|
||||
|
||||
const tooltip = Tooltip.getOrCreateInstance(elRef.current, config);
|
||||
elRef.current.addEventListener("show.bs.tooltip", () => {
|
||||
// Capture element now, since elRef.current may be null during cleanup.
|
||||
const element = elRef.current;
|
||||
|
||||
// Dispose any existing tooltip before creating a new one
|
||||
Tooltip.getInstance(element)?.dispose();
|
||||
|
||||
const tooltip = new Tooltip(element, config);
|
||||
element.addEventListener("show.bs.tooltip", () => {
|
||||
// Hide all the other tooltips.
|
||||
for (const otherTooltip of tooltips) {
|
||||
if (otherTooltip === tooltip) continue;
|
||||
@@ -878,12 +914,11 @@ export function useStaticTooltip(elRef: RefObject<Element>, config?: Partial<Too
|
||||
|
||||
return () => {
|
||||
tooltips.delete(tooltip);
|
||||
tooltip.dispose();
|
||||
// workaround for https://github.com/twbs/bootstrap/issues/37474
|
||||
(tooltip as any)._activeTrigger = {};
|
||||
(tooltip as any)._element = document.createElement('noscript'); // placeholder with no behavior
|
||||
if (element.isConnected) {
|
||||
tooltip.dispose();
|
||||
}
|
||||
|
||||
// Remove *all* tooltip elements from the DOM
|
||||
// Remove any lingering tooltip popup elements from the DOM.
|
||||
document
|
||||
.querySelectorAll('.tooltip')
|
||||
.forEach(t => t.remove());
|
||||
@@ -1385,7 +1420,7 @@ export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
|
||||
}
|
||||
|
||||
export function useColorScheme() {
|
||||
const themeStyle = getThemeStyle();
|
||||
const themeStyle = window.glob.getThemeStyle();
|
||||
const defaultValue = themeStyle === "auto" ? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) : themeStyle === "dark";
|
||||
const [ prefersDark, setPrefersDark ] = useState(defaultValue);
|
||||
|
||||
@@ -1400,12 +1435,3 @@ export function useColorScheme() {
|
||||
|
||||
return prefersDark ? "dark" : "light";
|
||||
}
|
||||
|
||||
function getThemeStyle() {
|
||||
const style = window.getComputedStyle(document.body);
|
||||
const themeStyle = style.getPropertyValue("--theme-style");
|
||||
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
|
||||
return themeStyle as "light" | "dark";
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { CKTextEditor, ModelText } from "@triliumnext/ckeditor5";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import math from "../../services/math";
|
||||
import { randomString } from "../../services/utils";
|
||||
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor, useTriliumOptionJson } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import { HighlightsListOptions } from "../type_widgets/options/text_notes";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
@@ -84,20 +86,11 @@ function AbstractHighlightsList<T extends RawHighlight>({ highlights, scrollToHi
|
||||
{filteredHighlights.length > 0 ? (
|
||||
<ol>
|
||||
{filteredHighlights.map(highlight => (
|
||||
<li
|
||||
<HighlightItem
|
||||
key={highlight.id}
|
||||
highlight={highlight}
|
||||
onClick={() => scrollToHighlight(highlight)}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: highlight.attrs.bold ? "700" : undefined,
|
||||
fontStyle: highlight.attrs.italic ? "italic" : undefined,
|
||||
textDecoration: highlight.attrs.underline ? "underline" : undefined,
|
||||
color: highlight.attrs.color,
|
||||
backgroundColor: highlight.attrs.background
|
||||
}}
|
||||
>{highlight.text}</span>
|
||||
</li>
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
@@ -112,6 +105,43 @@ function AbstractHighlightsList<T extends RawHighlight>({ highlights, scrollToHi
|
||||
);
|
||||
}
|
||||
|
||||
function HighlightItem<T extends RawHighlight>({ highlight, onClick }: {
|
||||
highlight: T;
|
||||
onClick(): void;
|
||||
}) {
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Render math equations after component mounts/updates
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
const mathElements = contentRef.current.querySelectorAll(".math-tex");
|
||||
|
||||
for (const mathEl of mathElements ?? []) {
|
||||
try {
|
||||
math.render(mathEl.textContent || "", mathEl as HTMLElement);
|
||||
} catch (e) {
|
||||
console.warn("Failed to render math in highlights:", e);
|
||||
}
|
||||
}
|
||||
}, [highlight.text]);
|
||||
|
||||
return (
|
||||
<li onClick={onClick}>
|
||||
<RawHtml
|
||||
containerRef={contentRef}
|
||||
style={{
|
||||
fontWeight: highlight.attrs.bold ? "700" : undefined,
|
||||
fontStyle: highlight.attrs.italic ? "italic" : undefined,
|
||||
textDecoration: highlight.attrs.underline ? "underline" : undefined,
|
||||
color: highlight.attrs.color,
|
||||
backgroundColor: highlight.attrs.background
|
||||
}}
|
||||
html={highlight.text}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
//#region Editable text (CKEditor)
|
||||
interface CKHighlight extends RawHighlight {
|
||||
textNode: ModelText;
|
||||
@@ -201,9 +231,24 @@ function extractHighlightsFromTextEditor(editor: CKTextEditor) {
|
||||
};
|
||||
|
||||
if (Object.values(attrs).some(Boolean)) {
|
||||
// Get HTML content from DOM (includes nested elements like math)
|
||||
let html = item.data;
|
||||
try {
|
||||
const modelPos = editor.model.createPositionAt(item.textNode, "before");
|
||||
const viewPos = editor.editing.mapper.toViewPosition(modelPos);
|
||||
const domPos = editor.editing.view.domConverter.viewPositionToDom(viewPos);
|
||||
if (domPos?.parent instanceof HTMLElement) {
|
||||
// Get the formatting span's innerHTML (includes math elements)
|
||||
html = domPos.parent.innerHTML;
|
||||
}
|
||||
} catch {
|
||||
// During change:data events, the view may not be fully synchronized with the model.
|
||||
// Fall back to using the raw text data.
|
||||
}
|
||||
|
||||
result.push({
|
||||
id: randomString(),
|
||||
text: item.data,
|
||||
text: html,
|
||||
attrs,
|
||||
textNode: item.textNode,
|
||||
offset: item.startOffset
|
||||
|
||||
@@ -87,7 +87,7 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
|
||||
// Render math equations after component mounts/updates
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
|
||||
const mathElements = contentRef.current.querySelectorAll(".math-tex");
|
||||
|
||||
for (const mathEl of mathElements ?? []) {
|
||||
try {
|
||||
|
||||
@@ -903,7 +903,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
loadResults.isNoteReloaded(noteContext.noteId) ||
|
||||
loadResults
|
||||
.getAttributeRows()
|
||||
.find((attr) => ["workspace", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name || "") && attributeService.isAffecting(attr, noteContext.note))
|
||||
.find((attr) => ["workspace", "iconClass", "workspaceIconClass", "workspaceTabBackgroundColor"].includes(attr.name || "") && attributeService.isAffecting(attr, noteContext.note))
|
||||
) {
|
||||
const $tab = this.getTabById(noteContext.ntxId);
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ function DesktopWebView({ src, ntxId }: { src: string, ntxId: string | null | un
|
||||
return <webview
|
||||
ref={webviewRef}
|
||||
src={src}
|
||||
key={src}
|
||||
class="note-detail-web-view-content"
|
||||
/>;
|
||||
}
|
||||
@@ -80,6 +81,7 @@ function BrowserWebView({ src, ntxId }: { src: string, ntxId: string | null | un
|
||||
return <iframe
|
||||
ref={iframeRef}
|
||||
src={src}
|
||||
key={src}
|
||||
class="note-detail-web-view-content"
|
||||
sandbox="allow-same-origin allow-scripts allow-popups" />;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
|
||||
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"] | null> = {
|
||||
ar: "ar-SA",
|
||||
cn: "zh-CN",
|
||||
cs: "cs-CZ",
|
||||
de: "de-DE",
|
||||
en: "en",
|
||||
"en-GB": "en",
|
||||
|
||||
@@ -3,6 +3,7 @@ import "./appearance.css";
|
||||
import { FontFamily, OptionNames } from "@triliumnext/commons";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import zoomService from "../../../components/zoom";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils";
|
||||
@@ -14,9 +15,10 @@ import FormGroup from "../../react/FormGroup";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import PlatformIndicator from "./components/PlatformIndicator";
|
||||
import RadioWithIllustration from "./components/RadioWithIllustration";
|
||||
@@ -333,20 +335,23 @@ function Font({ title, fontFamilyOption, fontSizeOption }: { title: string, font
|
||||
}
|
||||
|
||||
function ElectronIntegration() {
|
||||
const [ zoomFactor, setZoomFactor ] = useTriliumOption("zoomFactor");
|
||||
const [ zoomFactor ] = useTriliumOption("zoomFactor");
|
||||
const [ nativeTitleBarVisible, setNativeTitleBarVisible ] = useTriliumOptionBool("nativeTitleBarVisible");
|
||||
const [ backgroundEffects, setBackgroundEffects ] = useTriliumOptionBool("backgroundEffects");
|
||||
|
||||
const zoomPercentage = Math.round(parseFloat(zoomFactor || "1") * 100);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("electron_integration.desktop-application")}>
|
||||
<FormGroup name="zoom-factor" label={t("electron_integration.zoom-factor")} description={t("zoom_factor.description")}>
|
||||
<FormTextBox
|
||||
<OptionsRow name="zoom-factor" label={t("electron_integration.zoom-factor")} description={t("zoom_factor.description")}>
|
||||
<FormTextBoxWithUnit
|
||||
type="number"
|
||||
min="0.3" max="2.0" step="0.1"
|
||||
currentValue={zoomFactor} onChange={setZoomFactor}
|
||||
min={50} max={200} step={10}
|
||||
currentValue={String(zoomPercentage)}
|
||||
onChange={(v) => zoomService.setZoomFactorAndSave(parseInt(v, 10) / 100)}
|
||||
unit={t("units.percentage")}
|
||||
/>
|
||||
</FormGroup>
|
||||
<hr/>
|
||||
</OptionsRow>
|
||||
|
||||
<FormGroup name="native-title-bar" description={t("electron_integration.native-title-bar-description")}>
|
||||
<FormCheckbox
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { BackupDatabaseNowResponse, DatabaseBackup } from "@triliumnext/commons";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { formatDateTime } from "../../../utils/formatters";
|
||||
import Button from "../../react/Button";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import { FormMultiGroup } from "../../react/FormGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import { useTriliumOptionBool } from "../../react/hooks";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
import { formatDateTime } from "../../../utils/formatters";
|
||||
|
||||
export default function BackupSettings() {
|
||||
const [ backups, setBackups ] = useState<DatabaseBackup[]>([]);
|
||||
@@ -35,7 +36,7 @@ export default function BackupSettings() {
|
||||
<BackupNow refreshCallback={refreshBackups} />
|
||||
<BackupList backups={backups} />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function AutomaticBackup() {
|
||||
@@ -67,7 +68,7 @@ export function AutomaticBackup() {
|
||||
|
||||
<FormText>{t("backup.backup_recommendation")}</FormText>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function BackupNow({ refreshCallback }: { refreshCallback: () => void }) {
|
||||
@@ -82,7 +83,7 @@ export function BackupNow({ refreshCallback }: { refreshCallback: () => void })
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
|
||||
@@ -92,11 +93,13 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
|
||||
<colgroup>
|
||||
<col width="33%" />
|
||||
<col />
|
||||
<col width="1%" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("backup.date-and-time")}</th>
|
||||
<th>{t("backup.path")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -105,15 +108,20 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
|
||||
<tr>
|
||||
<td>{mtime ? formatDateTime(mtime) : "-"}</td>
|
||||
<td className="selectable-text">{filePath}</td>
|
||||
<td>
|
||||
<a href={`api/database/backup/download?filePath=${encodeURIComponent(filePath)}`} download>
|
||||
<Button text={t("backup.download")} />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td className="empty-table-placeholder" colspan={2}>{t("backup.no_backup_yet")}</td>
|
||||
<td className="empty-table-placeholder" colspan={3}>{t("backup.no_backup_yet")}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,3 +45,15 @@
|
||||
.option-row.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.option-row-link.use-tn-links {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
margin-inline: calc(-1 * var(--options-card-padding, 15px));
|
||||
padding-inline: var(--options-card-padding, 15px);
|
||||
transition: background-color 250ms ease-in-out;
|
||||
}
|
||||
|
||||
.option-row-link:hover {
|
||||
background: var(--hover-item-background-color);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { cloneElement, VNode } from "preact";
|
||||
import "./OptionsRow.css";
|
||||
|
||||
import { cloneElement, VNode } from "preact";
|
||||
|
||||
import { useUniqueName } from "../../../react/hooks";
|
||||
|
||||
interface OptionsRowProps {
|
||||
@@ -25,4 +27,24 @@ export default function OptionsRow({ name, label, description, children, centere
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface OptionsRowLinkProps {
|
||||
label: string;
|
||||
description?: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export function OptionsRowLink({ label, description, href }: OptionsRowLinkProps) {
|
||||
return (
|
||||
<a href={href} className="option-row option-row-link use-tn-links no-tooltip-preview">
|
||||
<div className="option-row-label">
|
||||
<label style={{ cursor: "pointer" }}>{label}</label>
|
||||
{description && <small className="option-row-description">{description}</small>}
|
||||
</div>
|
||||
<div className="option-row-input">
|
||||
<span className="bx bx-chevron-right" />
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,36 @@
|
||||
import OptionsSection from "./OptionsSection";
|
||||
import type { OptionPages } from "../../ContentWidget";
|
||||
import { t } from "../../../../services/i18n";
|
||||
import type { OptionPages } from "../../ContentWidget";
|
||||
import { OptionsRowLink } from "./OptionsRow";
|
||||
import OptionsSection from "./OptionsSection";
|
||||
|
||||
interface RelatedSettingsItem {
|
||||
title: string;
|
||||
description?: string;
|
||||
targetPage: OptionPages;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface RelatedSettingsProps {
|
||||
items: {
|
||||
title: string;
|
||||
targetPage: OptionPages;
|
||||
}[];
|
||||
items: RelatedSettingsItem[];
|
||||
}
|
||||
|
||||
export default function RelatedSettings({ items }: RelatedSettingsProps) {
|
||||
const filteredItems = items.filter(item => item.enabled !== false);
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("settings.related_settings")}>
|
||||
<nav className="use-tn-links" style={{ padding: 0, margin: 0, listStyleType: "none" }}>
|
||||
{items.map(item => (
|
||||
<li>
|
||||
<a href={`#root/_hidden/_options/${item.targetPage}`}>{item.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</nav>
|
||||
{filteredItems.map((item) => (
|
||||
<OptionsRowLink
|
||||
key={item.targetPage}
|
||||
label={item.title}
|
||||
description={item.description}
|
||||
href={`#root/_hidden/_options/${item.targetPage}`}
|
||||
/>
|
||||
))}
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { useTriliumOption, useTriliumOptionJson } from "../../react/hooks";
|
||||
import type { Locale } from "@triliumnext/commons";
|
||||
import { restartDesktopApp } from "../../../services/utils";
|
||||
import { isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import FormText from "../../react/FormText";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import Admonition from "../../react/Admonition";
|
||||
import Button from "../../react/Button";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import RelatedSettings from "./components/RelatedSettings";
|
||||
import { LocaleSelector } from "./components/LocaleSelector";
|
||||
|
||||
export default function InternationalizationOptions() {
|
||||
@@ -19,8 +20,17 @@ export default function InternationalizationOptions() {
|
||||
<>
|
||||
<LocalizationOptions />
|
||||
<ContentLanguages />
|
||||
{isElectron() && (
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
title: t("spellcheck.title"),
|
||||
description: t("spellcheck.related_description"),
|
||||
targetPage: "_optionsSpellcheck"
|
||||
}
|
||||
]} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function LocalizationOptions() {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { isElectron } from "../../../services/utils";
|
||||
import { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
@@ -93,7 +94,8 @@ function OcrSettings() {
|
||||
<RelatedSettings items={[
|
||||
{
|
||||
title: t("images.ocr_related_content_languages"),
|
||||
targetPage: "_optionsLocalization"
|
||||
targetPage: "_optionsLocalization",
|
||||
enabled: isElectron(), // This setting is only relevant for desktop, as web browsers use their own native OCR which doesn't support language selection.
|
||||
}
|
||||
]} />
|
||||
</>
|
||||
|
||||
@@ -1,63 +1,132 @@
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { useCallback, useMemo } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import { t } from "../../../services/i18n";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import { dynamicRequire, isElectron, restartDesktopApp } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import FormToggle from "../../react/FormToggle";
|
||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||
import NoItems from "../../react/NoItems";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { dynamicRequire, isElectron } from "../../../services/utils";
|
||||
|
||||
export default function SpellcheckSettings() {
|
||||
if (isElectron()) {
|
||||
return <ElectronSpellcheckSettings />
|
||||
} else {
|
||||
return <WebSpellcheckSettings />
|
||||
return <ElectronSpellcheckSettings />;
|
||||
}
|
||||
return <WebSpellcheckSettings />;
|
||||
}
|
||||
|
||||
interface SpellcheckLanguage {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function ElectronSpellcheckSettings() {
|
||||
const [ spellCheckEnabled, setSpellCheckEnabled ] = useTriliumOptionBool("spellCheckEnabled");
|
||||
|
||||
return (
|
||||
<>
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<FormText>{t("spellcheck.restart-required")}</FormText>
|
||||
|
||||
<OptionsRow name="spell-check-enabled" label={t("spellcheck.enable")}>
|
||||
<FormToggle
|
||||
switchOnName="" switchOffName=""
|
||||
currentValue={spellCheckEnabled}
|
||||
onChange={setSpellCheckEnabled}
|
||||
/>
|
||||
</OptionsRow>
|
||||
|
||||
<OptionsRow name="restart" centered>
|
||||
<Button
|
||||
name="restart-app-button"
|
||||
text={t("electron_integration.restart-app-button")}
|
||||
size="micro"
|
||||
onClick={restartDesktopApp}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
|
||||
{spellCheckEnabled && <SpellcheckLanguages />}
|
||||
{spellCheckEnabled && <CustomDictionary />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SpellcheckLanguages() {
|
||||
const [ spellCheckLanguageCode, setSpellCheckLanguageCode ] = useTriliumOption("spellCheckLanguageCode");
|
||||
|
||||
const availableLanguageCodes = useMemo(() => {
|
||||
const selectedCodes = useMemo(() =>
|
||||
(spellCheckLanguageCode ?? "")
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter((c) => c.length > 0),
|
||||
[spellCheckLanguageCode]
|
||||
);
|
||||
|
||||
const setSelectedCodes = useCallback((codes: string[]) => {
|
||||
setSpellCheckLanguageCode(codes.join(", "));
|
||||
}, [setSpellCheckLanguageCode]);
|
||||
|
||||
const availableLanguages = useMemo<SpellcheckLanguage[]>(() => {
|
||||
if (!isElectron()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
return webContents.session.availableSpellCheckerLanguages as string[];
|
||||
}, [])
|
||||
const { webContents } = dynamicRequire("@electron/remote").getCurrentWindow();
|
||||
const codes = webContents.session.availableSpellCheckerLanguages as string[];
|
||||
const displayNames = new Intl.DisplayNames([navigator.language], { type: "language" });
|
||||
|
||||
return codes.map((code) => ({
|
||||
code,
|
||||
name: displayNames.of(code) ?? code
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<FormText>{t("spellcheck.restart-required")}</FormText>
|
||||
|
||||
<FormCheckbox
|
||||
name="spell-check-enabled"
|
||||
label={t("spellcheck.enable")}
|
||||
currentValue={spellCheckEnabled} onChange={setSpellCheckEnabled}
|
||||
<OptionsSection title={t("spellcheck.language_code_label")}>
|
||||
<CheckboxList
|
||||
values={availableLanguages}
|
||||
keyProperty="code" titleProperty="name"
|
||||
currentValue={selectedCodes}
|
||||
onChange={setSelectedCodes}
|
||||
columnWidth="200px"
|
||||
/>
|
||||
|
||||
<FormGroup name="spell-check-languages" label={t("spellcheck.language_code_label")} description={t("spellcheck.multiple_languages_info")}>
|
||||
<FormTextBox
|
||||
placeholder={t("spellcheck.language_code_placeholder")}
|
||||
currentValue={spellCheckLanguageCode} onChange={setSpellCheckLanguageCode}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormText>
|
||||
<strong>{t("spellcheck.available_language_codes_label")} </strong>
|
||||
{availableLanguageCodes.join(", ")}
|
||||
</FormText>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function CustomDictionary() {
|
||||
function openDictionary() {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: "_customDictionary" });
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.custom_dictionary_title")}>
|
||||
<FormText>{t("spellcheck.custom_dictionary_description")}</FormText>
|
||||
|
||||
<OptionsRow name="custom-dictionary" label={t("spellcheck.custom_dictionary_edit")} description={t("spellcheck.custom_dictionary_edit_description")}>
|
||||
<Button
|
||||
name="open-custom-dictionary"
|
||||
text={t("spellcheck.custom_dictionary_open")}
|
||||
icon="bx bx-edit"
|
||||
onClick={openDictionary}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function WebSpellcheckSettings() {
|
||||
return (
|
||||
<OptionsSection title={t("spellcheck.title")}>
|
||||
<p>{t("spellcheck.description")}</p>
|
||||
<OptionsSection>
|
||||
<NoItems
|
||||
text={t("spellcheck.description")}
|
||||
icon="bx bx-check-double"
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { SyncTestResponse } from "@triliumnext/commons";
|
||||
import { useRef } from "preact/hooks";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { openInAppHelpFromUrl } from "../../../services/utils";
|
||||
import Button from "../../react/Button";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { useTriliumOptions } from "../../react/hooks";
|
||||
import FormText from "../../react/FormText";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import { SyncTestResponse } from "@triliumnext/commons";
|
||||
import FormTextBox from "../../react/FormTextBox";
|
||||
import { useTriliumOptions } from "../../react/hooks";
|
||||
import RawHtml from "../../react/RawHtml";
|
||||
import OptionsRow from "./components/OptionsRow";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import TimeSelector from "./components/TimeSelector";
|
||||
|
||||
export default function SyncOptions() {
|
||||
return (
|
||||
@@ -18,13 +21,12 @@ export default function SyncOptions() {
|
||||
<SyncConfiguration />
|
||||
<SyncTest />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function SyncConfiguration() {
|
||||
const [ options, setOptions ] = useTriliumOptions("syncServerHost", "syncServerTimeout", "syncProxy");
|
||||
const [ options, setOptions ] = useTriliumOptions("syncServerHost", "syncProxy");
|
||||
const syncServerHost = useRef(options.syncServerHost);
|
||||
const syncServerTimeout = useRef(options.syncServerTimeout);
|
||||
const syncProxy = useRef(options.syncProxy);
|
||||
|
||||
return (
|
||||
@@ -32,13 +34,12 @@ export function SyncConfiguration() {
|
||||
<form onSubmit={(e) => {
|
||||
setOptions({
|
||||
syncServerHost: syncServerHost.current,
|
||||
syncServerTimeout: syncServerTimeout.current,
|
||||
syncProxy: syncProxy.current
|
||||
});
|
||||
e.preventDefault();
|
||||
}}>
|
||||
<FormGroup name="sync-server-host" label={t("sync_2.server_address")}>
|
||||
<FormTextBox
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={syncServerHost.current} onChange={(newValue) => syncServerHost.current = newValue}
|
||||
/>
|
||||
@@ -50,27 +51,30 @@ export function SyncConfiguration() {
|
||||
<RawHtml html={t("sync_2.special_value_description")} />
|
||||
</>}
|
||||
>
|
||||
<FormTextBox
|
||||
<FormTextBox
|
||||
placeholder="https://<host>:<port>"
|
||||
currentValue={syncProxy.current} onChange={(newValue) => syncProxy.current = newValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="sync-server-timeout" label={t("sync_2.timeout")}>
|
||||
<FormTextBoxWithUnit
|
||||
min={1} max={10000000} type="number"
|
||||
unit={t("sync_2.timeout_unit")}
|
||||
currentValue={syncServerTimeout.current} onChange={(newValue) => syncServerTimeout.current = newValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "spaceBetween"}}>
|
||||
<Button text={t("sync_2.save")} kind="primary" />
|
||||
<Button text={t("sync_2.help")} onClick={() => openInAppHelpFromUrl("cbkrhQjrkKrh")} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr/>
|
||||
|
||||
<OptionsRow name="sync-server-timeout" label={t("sync_2.timeout")} description={t("sync_2.timeout_description")}>
|
||||
<TimeSelector
|
||||
name="sync-server-timeout"
|
||||
optionValueId="syncServerTimeout"
|
||||
optionTimeScaleId="syncServerTimeoutTimeScale"
|
||||
minimumSeconds={1}
|
||||
/>
|
||||
</OptionsRow>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function SyncTest() {
|
||||
@@ -90,5 +94,5 @@ export function SyncTest() {
|
||||
}}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ import { HTMLProps } from "preact/compat";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import attribute_autocomplete from "../../../services/attribute_autocomplete";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import utils from "../../../services/utils";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
@@ -23,7 +21,7 @@ import { buildRelationContextMenuHandler } from "./context_menu";
|
||||
import { JsPlumb } from "./jsplumb";
|
||||
import { NoteBox } from "./NoteBox";
|
||||
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
|
||||
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
|
||||
import { getMousePosition, getZoom, idToNoteId, noteIdToId, promptForRelationName } from "./utils";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
@@ -415,27 +413,7 @@ function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObjec
|
||||
// if there's no event, then this has been triggered programmatically
|
||||
if (!originalEvent || !mapApiRef.current) return;
|
||||
|
||||
const name = await dialog.prompt({
|
||||
message: t("relation_map.specify_new_relation_name"),
|
||||
shown: ({ $answer }) => {
|
||||
if (!$answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$answer.on("keyup", () => {
|
||||
// invalid characters are simply ignored (from user perspective they are not even entered)
|
||||
const attrName = utils.filterAttributeName($answer.val() as string);
|
||||
|
||||
$answer.val(attrName);
|
||||
});
|
||||
|
||||
attribute_autocomplete.initAttributeNameAutocomplete({
|
||||
$el: $answer,
|
||||
attributeType: "relation",
|
||||
open: true
|
||||
});
|
||||
}
|
||||
});
|
||||
const name = await promptForRelationName();
|
||||
|
||||
// Delete the newly created connection if the dialog was dismissed.
|
||||
if (!name || !name.trim()) {
|
||||
|
||||
@@ -75,6 +75,29 @@ export default class RelationMapApi {
|
||||
this.onDataChange(true);
|
||||
}
|
||||
|
||||
async renameRelation(connection: Connection, newName: string) {
|
||||
newName = utils.filterAttributeName(newName);
|
||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
||||
|
||||
if (!relation) return false;
|
||||
|
||||
// Check if a relation with the new name already exists between these notes.
|
||||
const exists = this.relations.some(
|
||||
(rel) => rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId && rel.name === newName
|
||||
);
|
||||
if (exists) return false;
|
||||
|
||||
await server.put(`notes/${relation.sourceNoteId}/relations/${newName}/to/${relation.targetNoteId}`);
|
||||
await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`);
|
||||
this.onDataChange(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
getRelationName(connection: Connection): string | undefined {
|
||||
const relation = this.relations.find((rel) => rel.attributeId === connection.id);
|
||||
return relation?.name;
|
||||
}
|
||||
|
||||
cleanupOtherNotes(noteIds: string[]) {
|
||||
const filteredNotes = this.data.notes.filter((note) => noteIds.includes(note.noteId));
|
||||
if (filteredNotes.length === this.data.notes.length) return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import RelationMapApi from "./api";
|
||||
import { promptForRelationName } from "./utils";
|
||||
|
||||
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
|
||||
return (e: MouseEvent) => {
|
||||
@@ -73,9 +74,25 @@ export function buildRelationContextMenuHandler(connection: Connection, mapApiRe
|
||||
contextMenu.show({
|
||||
x: event.pageX,
|
||||
y: event.pageY,
|
||||
items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }],
|
||||
items: [
|
||||
{ title: t("relation_map.rename_relation"), command: "rename", uiIcon: "bx bx-pencil" },
|
||||
{ kind: "separator" },
|
||||
{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }
|
||||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "remove") {
|
||||
if (command === "rename") {
|
||||
const currentName = mapApiRef.current?.getRelationName(connection) ?? "";
|
||||
const newName = await promptForRelationName(currentName);
|
||||
|
||||
if (!newName?.trim() || newName === currentName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mapApiRef.current?.renameRelation(connection, newName);
|
||||
if (!result) {
|
||||
await dialog.info(t("relation_map.connection_exists", { name: newName }));
|
||||
}
|
||||
} else if (command === "remove") {
|
||||
if (!(await dialog.confirm(t("relation_map.confirm_remove_relation")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import attribute_autocomplete from "../../../services/attribute_autocomplete";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import utils from "../../../services/utils";
|
||||
|
||||
export function noteIdToId(noteId: string) {
|
||||
return `rel-map-note-${noteId}`;
|
||||
@@ -32,3 +35,26 @@ export function getMousePosition(evt: MouseEvent, container: HTMLDivElement, zoo
|
||||
y: ((evt.clientY ?? 0) - rect.top) / zoom
|
||||
};
|
||||
}
|
||||
|
||||
export function promptForRelationName(defaultValue?: string): Promise<string | null> {
|
||||
return dialog.prompt({
|
||||
message: t("relation_map.specify_new_relation_name"),
|
||||
defaultValue,
|
||||
shown: ({ $answer }) => {
|
||||
if (!$answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$answer.on("keyup", () => {
|
||||
const attrName = utils.filterAttributeName($answer.val() as string);
|
||||
$answer.val(attrName);
|
||||
});
|
||||
|
||||
attribute_autocomplete.initAttributeNameAutocomplete({
|
||||
$el: $answer,
|
||||
attributeType: "relation",
|
||||
open: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,4 +55,14 @@ body.mobile .note-detail-readonly-text {
|
||||
|
||||
.edit-text-note-button:hover {
|
||||
border-color: var(--button-border-color);
|
||||
}
|
||||
|
||||
/* Inline code click-to-copy */
|
||||
.note-detail-readonly-text-content code.copyable-inline-code {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.note-detail-readonly-text-content code.copyable-inline-code:hover {
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
@@ -182,9 +182,21 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
marker: "@",
|
||||
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
|
||||
itemRenderer: (item) => {
|
||||
const suggestion = item as Suggestion;
|
||||
const itemElement = document.createElement("button");
|
||||
|
||||
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
|
||||
const iconElement = document.createElement("span");
|
||||
// Choose appropriate icon based on action
|
||||
let iconClass = suggestion.icon ?? "bx bx-note";
|
||||
if (suggestion.action === "create-note") {
|
||||
iconClass = "bx bx-plus";
|
||||
}
|
||||
iconElement.className = iconClass;
|
||||
|
||||
itemElement.append(iconElement, document.createTextNode(" "));
|
||||
const titleContainer = document.createElement("span");
|
||||
titleContainer.innerHTML = suggestion.highlightedNotePathTitle ?? "";
|
||||
itemElement.append(...titleContainer.childNodes, document.createTextNode(" "));
|
||||
|
||||
return itemElement;
|
||||
},
|
||||
|
||||
@@ -176,7 +176,9 @@ const config: ForgeConfig = {
|
||||
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
|
||||
[FuseV1Options.EnableNodeCliInspectArguments]: false,
|
||||
[FuseV1Options.EnableCookieEncryption]: true,
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true
|
||||
[FuseV1Options.OnlyLoadAppFromAsar]: true,
|
||||
[FuseV1Options.GrantFileProtocolExtraPrivileges]: false,
|
||||
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
"@electron-forge/maker-zip": "7.11.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
|
||||
"@electron-forge/plugin-fuses": "7.11.1",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@electron/fuses": "2.1.1",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"@types/electron-squirrel-startup": "1.0.2",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "40.8.5",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"electron": "41.1.1",
|
||||
"prebuild-install": "7.1.3"
|
||||
}
|
||||
}
|
||||
2
apps/edit-docs/demo/!!!meta.json
vendored
2
apps/edit-docs/demo/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"formatVersion": 2,
|
||||
"appVersion": "0.100.0",
|
||||
"appVersion": "0.102.2",
|
||||
"files": [
|
||||
{
|
||||
"isClone": false,
|
||||
|
||||
26
apps/edit-docs/demo/root/Trilium Demo.html
vendored
26
apps/edit-docs/demo/root/Trilium Demo.html
vendored
@@ -18,30 +18,23 @@
|
||||
width="150" height="150">
|
||||
</figure>
|
||||
<p><strong>Welcome to Trilium Notes!</strong>
|
||||
|
||||
</p>
|
||||
<p>This is a "demo" document packaged with Trilium to showcase some of its
|
||||
features and also give you some ideas on how you might structure your notes.
|
||||
You can play with it, and modify the note content and tree structure as
|
||||
you wish.</p>
|
||||
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
|
||||
our <a href="https://github.com/TriliumNext">GitHub repository</a>
|
||||
|
||||
</p>
|
||||
<h2>Cleanup</h2>
|
||||
|
||||
our <a href="https://github.com/TriliumNext">GitHub repository</a>.</p>
|
||||
<h2>Cleanup</h2>
|
||||
<p>Once you're finished with experimenting and want to cleanup these pages,
|
||||
you can simply delete them all.</p>
|
||||
<h2>Formatting</h2>
|
||||
|
||||
<h2>Formatting</h2>
|
||||
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
||||
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
||||
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
||||
<a
|
||||
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
||||
<h3>Lists</h3>
|
||||
|
||||
<h3>Lists</h3>
|
||||
<p><strong>Ordered:</strong>
|
||||
|
||||
</p>
|
||||
<ol>
|
||||
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
||||
@@ -56,7 +49,6 @@
|
||||
</li>
|
||||
</ol>
|
||||
<p><strong>Unordered:</strong>
|
||||
|
||||
</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
||||
@@ -66,8 +58,7 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Block quotes</h3>
|
||||
|
||||
<h3>Block quotes</h3>
|
||||
<blockquote>
|
||||
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
||||
<p>– Ludwig Wittgenstein</p>
|
||||
@@ -75,9 +66,8 @@
|
||||
<hr>
|
||||
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
|
||||
<a
|
||||
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and
|
||||
<a
|
||||
href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
||||
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists</a>, <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>,
|
||||
and <a href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -14,24 +14,19 @@
|
||||
|
||||
<div class="ck-content">
|
||||
<h2>Main characters</h2>
|
||||
|
||||
<p>… here put main characters …</p>
|
||||
<p> </p>
|
||||
<h2>Plot</h2>
|
||||
|
||||
<h2>Plot</h2>
|
||||
<p>… describe main plot lines …</p>
|
||||
<p> </p>
|
||||
<h2>Tone</h2>
|
||||
|
||||
<h2>Tone</h2>
|
||||
<p> </p>
|
||||
<h2>Genre</h2>
|
||||
|
||||
<h2>Genre</h2>
|
||||
<p>scifi / drama / romance</p>
|
||||
<p> </p>
|
||||
<h2>Similar books</h2>
|
||||
|
||||
<h2>Similar books</h2>
|
||||
<ul>
|
||||
<li>…</li>
|
||||
<li data-list-item-id="eebd9f297d5dc97dfc46579ba1f25d7bf">…</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
977
apps/edit-docs/demo/style.css
vendored
977
apps/edit-docs/demo/style.css
vendored
File diff suppressed because it is too large
Load Diff
@@ -17,20 +17,29 @@ async function main() {
|
||||
|
||||
await initializeTranslations();
|
||||
await initializeDatabase(true);
|
||||
|
||||
// Wait for becca to be loaded before importing data
|
||||
const beccaLoader = await import("@triliumnext/server/src/becca/becca_loader.js");
|
||||
await beccaLoader.beccaLoaded;
|
||||
|
||||
cls.init(async () => {
|
||||
await importData(DEMO_ZIP_DIR_PATH);
|
||||
setOptions();
|
||||
initializedPromise.resolve();
|
||||
});
|
||||
|
||||
initializedPromise.resolve();
|
||||
}
|
||||
|
||||
async function setOptions() {
|
||||
const optionsService = (await import("@triliumnext/server/src/services/options.js")).default;
|
||||
const sql = (await import("@triliumnext/server/src/services/sql.js")).default;
|
||||
|
||||
optionsService.setOption("eraseUnusedAttachmentsAfterSeconds", 10);
|
||||
optionsService.setOption("eraseUnusedAttachmentsAfterTimeScale", 60);
|
||||
optionsService.setOption("compressImages", "false");
|
||||
|
||||
// Set initial note to the first visible child of root (not _hidden)
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 AND noteId != '_hidden' ORDER BY notePosition") || "root";
|
||||
optionsService.setOption("openNoteContexts", JSON.stringify([{ notePath: startNoteId, active: true }]));
|
||||
}
|
||||
|
||||
async function registerHandlers() {
|
||||
|
||||
@@ -141,9 +141,15 @@ async function main() {
|
||||
|
||||
async function setOptions() {
|
||||
const optionsService = (await import("@triliumnext/server/src/services/options.js")).default;
|
||||
const sql = (await import("@triliumnext/server/src/services/sql.js")).default;
|
||||
|
||||
optionsService.setOption("eraseUnusedAttachmentsAfterSeconds", 10);
|
||||
optionsService.setOption("eraseUnusedAttachmentsAfterTimeScale", 60);
|
||||
optionsService.setOption("compressImages", "false");
|
||||
|
||||
// Set initial note to the first visible child of root (not _hidden)
|
||||
const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 AND noteId != '_hidden' ORDER BY notePosition") || "root";
|
||||
optionsService.setOption("openNoteContexts", JSON.stringify([{ notePath: startNoteId, active: true }]));
|
||||
}
|
||||
|
||||
async function exportData(noteId: string, format: ExportFormat, outputPath: string, ignoredFiles?: Set<string>) {
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "17.4.0"
|
||||
"dotenv": "17.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import test, { expect } from "@playwright/test";
|
||||
|
||||
import App from "./support/app";
|
||||
|
||||
test("Native Title Bar not displayed on web", async ({ page, context }) => {
|
||||
@@ -18,8 +19,6 @@ test("Tray settings not displayed on web", async ({ page, context }) => {
|
||||
test("Spellcheck settings not displayed on web", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto({ url: "http://localhost:8082/#root/_hidden/_options/_optionsSpellcheck" });
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Spell Check" })).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByRole("heading", { name: "Tray" })).toBeHidden();
|
||||
await expect(app.currentNoteSplitContent.getByText("These options apply only for desktop builds")).toBeVisible();
|
||||
await expect(app.currentNoteSplitContent.getByText("Enable spellcheck")).toBeHidden();
|
||||
await expect(app.currentNoteSplitContent.getByText("Check spelling")).toBeHidden();
|
||||
});
|
||||
|
||||
@@ -30,11 +30,11 @@
|
||||
"proxy-nginx-subdir": "docker run --name trilium-nginx-subdir --rm --network=host -v ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx:latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "3.0.64",
|
||||
"@ai-sdk/google": "3.0.55",
|
||||
"@ai-sdk/openai": "3.0.49",
|
||||
"@ai-sdk/anthropic": "3.0.67",
|
||||
"@ai-sdk/google": "3.0.59",
|
||||
"@ai-sdk/openai": "3.0.51",
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"ai": "6.0.142",
|
||||
"ai": "6.0.149",
|
||||
"better-sqlite3": "12.8.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"js-yaml": "4.1.1",
|
||||
@@ -78,7 +78,6 @@
|
||||
"@types/xml2js": "0.4.14",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"axios": "1.14.0",
|
||||
"chardet": "2.1.1",
|
||||
"cheerio": "1.2.0",
|
||||
"chokidar": "5.0.0",
|
||||
@@ -110,8 +109,8 @@
|
||||
"ini": "6.0.0",
|
||||
"is-animated": "2.0.2",
|
||||
"is-svg": "6.1.0",
|
||||
"jimp": "1.6.0",
|
||||
"marked": "17.0.5",
|
||||
"jimp": "1.6.1",
|
||||
"marked": "17.0.6",
|
||||
"mime-types": "3.0.2",
|
||||
"multer": "2.1.1",
|
||||
"normalize-strings": "1.1.1",
|
||||
@@ -131,7 +130,7 @@
|
||||
"tmp": "0.2.5",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "8.0.3",
|
||||
"vite": "8.0.5",
|
||||
"ws": "8.20.0",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.3.0"
|
||||
|
||||
@@ -51,7 +51,8 @@ VERSION=`jq -r ".version" package.json`
|
||||
ARCHIVE_NAME="TriliumNotes-Server-${VERSION}-linux-${ARCH}"
|
||||
echo "Creating Archive $ARCHIVE_NAME..."
|
||||
|
||||
mkdir $DIST_DIR
|
||||
rm -rf $DIST_DIR
|
||||
mkdir -p $DIST_DIR
|
||||
cp -r "$BUILD_DIR" "$DIST_DIR/$ARCHIVE_NAME"
|
||||
cd $DIST_DIR
|
||||
tar cJf "$ARCHIVE_NAME.tar.xz" "$ARCHIVE_NAME"
|
||||
|
||||
@@ -10,6 +10,7 @@ import helmet from "helmet";
|
||||
import { t } from "i18next";
|
||||
import path from "path";
|
||||
import favicon from "serve-favicon";
|
||||
import type serveStatic from "serve-static";
|
||||
|
||||
import assets from "./routes/assets.js";
|
||||
import custom from "./routes/custom.js";
|
||||
@@ -24,6 +25,9 @@ import { RESOURCE_DIR } from "./services/resource_dir.js";
|
||||
import sql_init from "./services/sql_init.js";
|
||||
import utils, { getResourceDir, isDev } from "./services/utils.js";
|
||||
|
||||
// Allow serving assets even if the installation path contains a hidden (dot-prefixed) directory.
|
||||
const STATIC_OPTIONS: serveStatic.ServeStaticOptions = { dotfiles: "allow" };
|
||||
|
||||
export default async function buildApp() {
|
||||
const app = express();
|
||||
|
||||
@@ -95,10 +99,10 @@ export default async function buildApp() {
|
||||
// localhost-only guard and does not require Trilium authentication.
|
||||
mcpRoutes.register(app);
|
||||
|
||||
app.use(express.static(path.join(publicDir, "root")));
|
||||
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest")));
|
||||
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
|
||||
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
|
||||
app.use(express.static(path.join(publicDir, "root"), STATIC_OPTIONS));
|
||||
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest"), STATIC_OPTIONS));
|
||||
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt"), STATIC_OPTIONS));
|
||||
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png"), STATIC_OPTIONS));
|
||||
|
||||
const { default: sessionParser, startSessionCleanup } = await import("./routes/session_parser.js");
|
||||
app.use(sessionParser);
|
||||
|
||||
Binary file not shown.
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user