Compare commits
389 Commits
feature/be
...
feat/extra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91237918d8 | ||
|
|
3f207663aa | ||
|
|
f466367c4d | ||
|
|
924d495d2e | ||
|
|
0b5c1b648b | ||
|
|
03ec39f52a | ||
|
|
f3ae1bf12c | ||
|
|
6d61cf41d9 | ||
|
|
333679523b | ||
|
|
1cd3a361f6 | ||
|
|
7405627663 | ||
|
|
2c4fb6c0d0 | ||
|
|
3e284208ef | ||
|
|
42b7f4c795 | ||
|
|
d445209eeb | ||
|
|
ed1bf17add | ||
|
|
4800f2a172 | ||
|
|
e7ff364c01 | ||
|
|
79ca299726 | ||
|
|
9d7ba48a6a | ||
|
|
9329665919 | ||
|
|
f6821bce03 | ||
|
|
a7aedf93ab | ||
|
|
4ffcf01452 | ||
|
|
618459d353 | ||
|
|
6436c16449 | ||
|
|
1bec457004 | ||
|
|
3e541e37fe | ||
|
|
a41b78d36f | ||
|
|
79c50f3b4c | ||
|
|
0f54b01cdd | ||
|
|
41f2b03711 | ||
|
|
0be76f982c | ||
|
|
5deb277672 | ||
|
|
3d15c9e94c | ||
|
|
1363f94621 | ||
|
|
abdcd6cc0c | ||
|
|
dc8abed2f3 | ||
|
|
892c2cd838 | ||
|
|
2796b29138 | ||
|
|
05620a129f | ||
|
|
cb11955a44 | ||
|
|
c3623a15fb | ||
|
|
0273c64bbf | ||
|
|
5b37140ffa | ||
|
|
fb4d63b049 | ||
|
|
015e41e792 | ||
|
|
8e47f33329 | ||
|
|
ad4a8ec5f4 | ||
|
|
56356f9c61 | ||
|
|
abda0f9111 | ||
|
|
c2a9d21198 | ||
|
|
59ffa1fa93 | ||
|
|
d056185368 | ||
|
|
251c1f6471 | ||
|
|
9efca9827e | ||
|
|
f5e2129ad4 | ||
|
|
6c8e6f2429 | ||
|
|
9b4b1a393e | ||
|
|
028334407c | ||
|
|
b93540b40d | ||
|
|
9e7eba5eab | ||
|
|
fcfa64ae52 | ||
|
|
62f5b800b6 | ||
|
|
5b910cce56 | ||
|
|
a5e8c8f573 | ||
|
|
2c8edb413e | ||
|
|
644cc27fa7 | ||
|
|
71d3eb4fde | ||
|
|
7c2340d60e | ||
|
|
24013ef020 | ||
|
|
72d9e846b7 | ||
|
|
b572ea0954 | ||
|
|
060257fa06 | ||
|
|
1c6bb0a20e | ||
|
|
1479109582 | ||
|
|
13f4e38f48 | ||
|
|
5cbde8d32a | ||
|
|
f3e3ef2f7d | ||
|
|
0a58f8108a | ||
|
|
768213438a | ||
|
|
00e0eb6f8a | ||
|
|
3abea13d79 | ||
|
|
67ab7f0c1e | ||
|
|
b38e8e27b2 | ||
|
|
a70c103b93 | ||
|
|
b83c3090f7 | ||
|
|
59ee38e7a6 | ||
|
|
890fe5929b | ||
|
|
56cc312565 | ||
|
|
9dfd015a27 | ||
|
|
04618dcdab | ||
|
|
f408e15c32 | ||
|
|
21cb896849 | ||
|
|
b9bcb07b6d | ||
|
|
00e60c147c | ||
|
|
ad6fd64226 | ||
|
|
6595fd9c10 | ||
|
|
a268507b80 | ||
|
|
9e847f67f2 | ||
|
|
df4992122b | ||
|
|
a325ba7b8f | ||
|
|
26e64ae7d0 | ||
|
|
d793774f51 | ||
|
|
c5f2b5c177 | ||
|
|
b2f782f2a3 | ||
|
|
b65b31ca4d | ||
|
|
64827dcdcf | ||
|
|
96d5d07087 | ||
|
|
ffd0f4727a | ||
|
|
e33cd86d30 | ||
|
|
fc85c23a67 | ||
|
|
a55c8fb210 | ||
|
|
19b865a5b4 | ||
|
|
61d90dda36 | ||
|
|
a4b1f06475 | ||
|
|
49704ea928 | ||
|
|
a73df362d5 | ||
|
|
f42010e22a | ||
|
|
0b85e0fe2d | ||
|
|
c7f0720237 | ||
|
|
cc3031eaad | ||
|
|
2850c7808c | ||
|
|
30bbcd866f | ||
|
|
55b6f322ac | ||
|
|
4518c9bb99 | ||
|
|
c551c863f4 | ||
|
|
3c25f8b4f3 | ||
|
|
d3c37556c3 | ||
|
|
cf60fcd6c1 | ||
|
|
97075ff91b | ||
|
|
ce57a43d90 | ||
|
|
33485369c3 | ||
|
|
7fcd93a61b | ||
|
|
3d4b84c7c4 | ||
|
|
61eb4017dd | ||
|
|
b4df4aaf0d | ||
|
|
244294f699 | ||
|
|
d609ee028e | ||
|
|
8dc8b046fb | ||
|
|
349203e300 | ||
|
|
83a8f07998 | ||
|
|
94d4a307cf | ||
|
|
2e35e0a830 | ||
|
|
ecdb819067 | ||
|
|
e2cf0c6e3e | ||
|
|
5dd600a291 | ||
|
|
df1beb1ffb | ||
|
|
7773059ac0 | ||
|
|
a238fc16b2 | ||
|
|
e298f5ea6f | ||
|
|
a5512267c1 | ||
|
|
f503c4ca6c | ||
|
|
c834c01c8e | ||
|
|
1f72ab9593 | ||
|
|
c7d446f4aa | ||
|
|
fb1530423d | ||
|
|
545464efee | ||
|
|
29d0223fd1 | ||
|
|
00852277f2 | ||
|
|
edfe23d88c | ||
|
|
2b8a7a28d9 | ||
|
|
7f29480237 | ||
|
|
61ac482946 | ||
|
|
de6a6cbb07 | ||
|
|
2dcb003909 | ||
|
|
d5c934a518 | ||
|
|
779909837c | ||
|
|
a4cb375a0f | ||
|
|
e2da8c28ca | ||
|
|
7808848f05 | ||
|
|
7c05109645 | ||
|
|
8f9e89b73b | ||
|
|
861a61a4d8 | ||
|
|
52d4083814 | ||
|
|
0272189b22 | ||
|
|
ddba0e823c | ||
|
|
be81acb9e7 | ||
|
|
3bb97385c9 | ||
|
|
a72cec0494 | ||
|
|
cb02198c6f | ||
|
|
298d438230 | ||
|
|
cb2f7932dd | ||
|
|
3354bd669f | ||
|
|
8ad779be66 | ||
|
|
f462034868 | ||
|
|
26c25cd4cd | ||
|
|
6398830c2d | ||
|
|
0b065063f2 | ||
|
|
a3a9de6fdd | ||
|
|
d77d30f29e | ||
|
|
5cabc6379d | ||
|
|
af537e6a48 | ||
|
|
faf3797663 | ||
|
|
db57f3ff62 | ||
|
|
0f77caad69 | ||
|
|
751b91e1b8 | ||
|
|
968a17fbfb | ||
|
|
712e87b39f | ||
|
|
0b40b42315 | ||
|
|
62996b1162 | ||
|
|
b67ccc6091 | ||
|
|
211d2dcf99 | ||
|
|
ee52e16a75 | ||
|
|
0c27bd25fa | ||
|
|
b6a6e78d01 | ||
|
|
92e6a29e70 | ||
|
|
acc8cee7cd | ||
|
|
afefbe154b | ||
|
|
83fa55b7d9 | ||
|
|
4f6c10d995 | ||
|
|
ed972d2601 | ||
|
|
6b57ee5654 | ||
|
|
e469af1ca5 | ||
|
|
6d41f076c2 | ||
|
|
8cff591746 | ||
|
|
787b180378 | ||
|
|
b3ccf89094 | ||
|
|
d31c6b1627 | ||
|
|
1481356d1f | ||
|
|
a54661fd0a | ||
|
|
ae4a3f10ae | ||
|
|
fe3160e7a1 | ||
|
|
66659d4786 | ||
|
|
0b25b09040 | ||
|
|
0d41cc2660 | ||
|
|
f5e8822718 | ||
|
|
bdc220ec12 | ||
|
|
3eb68e5271 | ||
|
|
521952ebcc | ||
|
|
034091a696 | ||
|
|
ae881101d8 | ||
|
|
b11a30c49c | ||
|
|
4625efda7f | ||
|
|
3c168d750d | ||
|
|
5cc7b259ce | ||
|
|
f7ae046b20 | ||
|
|
02f43d6239 | ||
|
|
53e1fa1047 | ||
|
|
b1dc0e234f | ||
|
|
9d380dd828 | ||
|
|
1f77540dbb | ||
|
|
455edbfb5d | ||
|
|
7288b66d27 | ||
|
|
3d72ec80bb | ||
|
|
f2a74df511 | ||
|
|
68c6052d10 | ||
|
|
c4edb56bd4 | ||
|
|
b6a3fe7cfb | ||
|
|
7a088c5b7d | ||
|
|
2e845a9faa | ||
|
|
ac3ae0dbbe | ||
|
|
a3fc13de3a | ||
|
|
ee6cbc710c | ||
|
|
18d701525e | ||
|
|
e47c848ec8 | ||
|
|
cd64548299 | ||
|
|
8645d053de | ||
|
|
91f2dabed7 | ||
|
|
716612680d | ||
|
|
3800fb85eb | ||
|
|
d807984be4 | ||
|
|
2c92ae8898 | ||
|
|
3d8cbc81c4 | ||
|
|
d747c94450 | ||
|
|
a627d1f96e | ||
|
|
869db5e478 | ||
|
|
73e94d385e | ||
|
|
8f4ebeb335 | ||
|
|
263ee864be | ||
|
|
f078732624 | ||
|
|
fac1f6b16c | ||
|
|
a5841c1423 | ||
|
|
aaca18003d | ||
|
|
5ec521b024 | ||
|
|
b3c0be7559 | ||
|
|
d52b735b99 | ||
|
|
639b1f2863 | ||
|
|
aff4f7e010 | ||
|
|
dec4dafba6 | ||
|
|
d0cdcfc32c | ||
|
|
0867b81c7a | ||
|
|
bde6068f2d | ||
|
|
47fd2affa4 | ||
|
|
2dd541e1d0 | ||
|
|
7f2cc885fe | ||
|
|
19a365a370 | ||
|
|
9a50da328e | ||
|
|
181e36a7c1 | ||
|
|
178508d245 | ||
|
|
8157ef5e74 | ||
|
|
d132d084cf | ||
|
|
494b55d685 | ||
|
|
0185dd0d18 | ||
|
|
142ed42d90 | ||
|
|
5b95b9875b | ||
|
|
688d197472 | ||
|
|
b745fb476e | ||
|
|
047b5a85d2 | ||
|
|
370a0c6a05 | ||
|
|
0d4558fee1 | ||
|
|
76526e0a96 | ||
|
|
70093e0a7d | ||
|
|
458398f2ca | ||
|
|
7a6cc4f51e | ||
|
|
80404b83b0 | ||
|
|
f4ccce7de5 | ||
|
|
c612bdbfc1 | ||
|
|
f8b5417d6c | ||
|
|
3a9e686533 | ||
|
|
9e8d89a170 | ||
|
|
31c70938d6 | ||
|
|
07f3c48d0b | ||
|
|
2821b6da9d | ||
|
|
daba7c398d | ||
|
|
de1ef5b98b | ||
|
|
1bb206d978 | ||
|
|
2fd5ddab86 | ||
|
|
27dc662636 | ||
|
|
52691b5c8c | ||
|
|
8087ed5688 | ||
|
|
79e2c97882 | ||
|
|
1078107776 | ||
|
|
9c9e123e3d | ||
|
|
a8c2947062 | ||
|
|
366166a561 | ||
|
|
4d2b02eddb | ||
|
|
07871853a5 | ||
|
|
254145f0e5 | ||
|
|
c28f11336e | ||
|
|
2e30683b7b | ||
|
|
0af7b8b145 | ||
|
|
5d39b84886 | ||
|
|
537c4051cc | ||
|
|
d0a22bc517 | ||
|
|
19a75acf3f | ||
|
|
3f0abce874 | ||
|
|
36dd29f919 | ||
|
|
d7838f0b67 | ||
|
|
3353d4f436 | ||
|
|
7740154bdc | ||
|
|
87ab41c80c | ||
|
|
d2391f94c0 | ||
|
|
050ddb8c55 | ||
|
|
bc23e0984a | ||
|
|
07de353207 | ||
|
|
c02491d2e6 | ||
|
|
a6ede8f905 | ||
|
|
22941a9ce0 | ||
|
|
633a09d414 | ||
|
|
29f0881c5a | ||
|
|
60debca37b | ||
|
|
30ea81d0fb | ||
|
|
b1d92c4fe6 | ||
|
|
70f46de2d8 | ||
|
|
f1b2d0b870 | ||
|
|
8a385972fc | ||
|
|
28dd85c1d1 | ||
|
|
827c8e0e72 | ||
|
|
162c076a14 | ||
|
|
9386465de7 | ||
|
|
acca22f3a1 | ||
|
|
f8d84814e0 | ||
|
|
c46cf41842 | ||
|
|
64ab1c4116 | ||
|
|
a6de1041c7 | ||
|
|
c8d34e65ea | ||
|
|
51db729546 | ||
|
|
d2052ad236 | ||
|
|
9c4301467f | ||
|
|
e7355dc0e4 | ||
|
|
4110fec94f | ||
|
|
d5e601eae9 | ||
|
|
4f044c4a57 | ||
|
|
5821c350e1 | ||
|
|
edba8188fe | ||
|
|
1471a72633 | ||
|
|
56834cb88a | ||
|
|
a0f16f9184 | ||
|
|
de80eb4806 | ||
|
|
48a4b81fbe | ||
|
|
e225794f72 | ||
|
|
4eef30f8b5 | ||
|
|
569b09609d | ||
|
|
39838c25c2 | ||
|
|
49e90c08a9 | ||
|
|
e777b06fb8 | ||
|
|
497ec2ac74 | ||
|
|
c5d282d203 |
30
.github/workflows/i18n.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Internationalization
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "weblate:*"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/client/src/translations/**"
|
||||
- ".github/workflows/i18n.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
i18n-check:
|
||||
name: Check i18n translations
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- name: Set up node & dependencies
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Check translations
|
||||
run: pnpm tsx scripts/translation/check-translation-coverage.ts
|
||||
8
.github/workflows/release.yml
vendored
@@ -11,6 +11,14 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sanity-check:
|
||||
name: Sanity Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Check version consistency
|
||||
run: pnpm tsx ${{ github.workspace }}/scripts/check-version-consistency.ts ${{ github.ref_name }}
|
||||
make-electron:
|
||||
name: Make Electron
|
||||
strategy:
|
||||
|
||||
2
.github/workflows/website.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --filter website --frozen-lockfile
|
||||
run: pnpm install --filter website --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Build the website
|
||||
run: pnpm website:build
|
||||
|
||||
2
.gitignore
vendored
@@ -51,4 +51,4 @@ upload
|
||||
# docs
|
||||
site/
|
||||
apps/*/coverage
|
||||
scripts/translation/.language*.json
|
||||
scripts/translation/.language*.json
|
||||
|
||||
11
README.md
@@ -165,6 +165,17 @@ pnpm install
|
||||
pnpm edit-docs:edit-docs
|
||||
```
|
||||
|
||||
Alternatively, if you have Nix installed:
|
||||
```shell
|
||||
# Run directly
|
||||
nix run .#edit-docs
|
||||
|
||||
# Or install to your profile
|
||||
nix profile install .#edit-docs
|
||||
trilium-edit-docs
|
||||
```
|
||||
|
||||
|
||||
### Building the Executable
|
||||
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
|
||||
```shell
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"keywords": [],
|
||||
"author": "Elian Doran <contact@eliandoran.me>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"packageManager": "pnpm@10.28.0",
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "2.14.3",
|
||||
"@redocly/cli": "2.14.5",
|
||||
"archiver": "7.0.1",
|
||||
"fs-extra": "11.3.3",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"typedoc": "0.28.15",
|
||||
"typedoc": "0.28.16",
|
||||
"typedoc-plugin-missing-exports": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/client",
|
||||
"version": "0.101.1",
|
||||
"version": "0.101.3",
|
||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -44,7 +44,7 @@
|
||||
"draggabilly": "3.0.0",
|
||||
"force-graph": "1.51.0",
|
||||
"globals": "17.0.0",
|
||||
"i18next": "25.7.3",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-http-backend": "3.0.2",
|
||||
"jquery": "3.7.1",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
@@ -56,13 +56,12 @@
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "17.0.1",
|
||||
"mermaid": "11.12.2",
|
||||
"mind-elixir": "5.4.0",
|
||||
"mind-elixir": "5.5.0",
|
||||
"normalize.css": "8.0.1",
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.28.1",
|
||||
"react-i18next": "16.5.1",
|
||||
"react-window": "2.2.3",
|
||||
"react-zoom-pan-pinch": "3.7.0",
|
||||
"preact": "10.28.2",
|
||||
"react-i18next": "16.5.3",
|
||||
"react-window": "2.2.5",
|
||||
"reveal.js": "5.2.1",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@@ -70,7 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@prefresh/vite": "2.4.11",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/leaflet": "1.9.21",
|
||||
@@ -79,7 +78,8 @@
|
||||
"@types/reveal.js": "5.2.2",
|
||||
"@types/tabulator-tables": "6.3.1",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.11",
|
||||
"happy-dom": "20.1.0",
|
||||
"lightningcss": "1.30.2",
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.1.4"
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ export type CommandMappings = {
|
||||
};
|
||||
openInTab: ContextMenuCommandData;
|
||||
openNoteInSplit: ContextMenuCommandData;
|
||||
openNoteInWindow: ContextMenuCommandData;
|
||||
openNoteInPopup: ContextMenuCommandData;
|
||||
toggleNoteHoisting: ContextMenuCommandData;
|
||||
insertNoteAfter: ContextMenuCommandData;
|
||||
@@ -541,6 +542,7 @@ export type FilteredCommandNames<T extends CommandData> = keyof Pick<CommandMapp
|
||||
|
||||
export class AppContext extends Component {
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
components: Component[];
|
||||
beforeUnloadListeners: (WeakRef<BeforeUploadListener> | (() => boolean))[];
|
||||
tabManager!: TabManager;
|
||||
@@ -549,10 +551,11 @@ export class AppContext extends Component {
|
||||
|
||||
lastSearchString?: string;
|
||||
|
||||
constructor(isMainWindow: boolean) {
|
||||
constructor(isMainWindow: boolean, windowId: string) {
|
||||
super();
|
||||
|
||||
this.isMainWindow = isMainWindow;
|
||||
this.windowId = windowId;
|
||||
// non-widget/layout components needed for the application
|
||||
this.components = [];
|
||||
this.beforeUnloadListeners = [];
|
||||
@@ -682,8 +685,7 @@ export class AppContext extends Component {
|
||||
this.beforeUnloadListeners = this.beforeUnloadListeners.filter(l => l !== listener);
|
||||
}
|
||||
}
|
||||
|
||||
const appContext = new AppContext(window.glob.isMainWindow);
|
||||
const appContext = new AppContext(window.glob.isMainWindow, window.glob.windowId);
|
||||
|
||||
// we should save all outstanding changes before the page/app is closed
|
||||
$(window).on("beforeunload", () => {
|
||||
|
||||
@@ -142,14 +142,15 @@ export default class Entrypoints extends Component {
|
||||
}
|
||||
|
||||
async openInWindowCommand({ notePath, hoistedNoteId, viewScope }: NoteCommandData) {
|
||||
const extraWindowId = utils.randomString(4);
|
||||
const extraWindowHash = linkService.calculateHash({ notePath, hoistedNoteId, viewScope });
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const { ipcRenderer } = utils.dynamicRequire("electron");
|
||||
|
||||
ipcRenderer.send("create-extra-window", { extraWindowHash });
|
||||
ipcRenderer.send("create-extra-window", { extraWindowId, extraWindowHash });
|
||||
} else {
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1${extraWindowHash}`;
|
||||
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=${extraWindowId}${extraWindowHash}`;
|
||||
|
||||
window.open(url, "", "width=1000,height=800");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import linkService from "../services/link.js";
|
||||
import type { EventData } from "./app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
const MAX_SAVED_WINDOWS = 10;
|
||||
|
||||
interface TabState {
|
||||
contexts: NoteContext[];
|
||||
position: number;
|
||||
@@ -25,6 +27,13 @@ interface NoteContextState {
|
||||
viewScope: Record<string, any>;
|
||||
}
|
||||
|
||||
interface WindowState {
|
||||
windowId: string;
|
||||
createdAt: number;
|
||||
closedAt: number;
|
||||
contexts: NoteContextState[];
|
||||
}
|
||||
|
||||
export default class TabManager extends Component {
|
||||
public children: NoteContext[];
|
||||
public mutex: Mutex;
|
||||
@@ -41,9 +50,6 @@ export default class TabManager extends Component {
|
||||
this.recentlyClosedTabs = [];
|
||||
|
||||
this.tabsUpdate = new SpacedUpdate(async () => {
|
||||
if (!appContext.isMainWindow) {
|
||||
return;
|
||||
}
|
||||
if (options.is("databaseReadonly")) {
|
||||
return;
|
||||
}
|
||||
@@ -52,9 +58,21 @@ export default class TabManager extends Component {
|
||||
.map((nc) => nc.getPojoState())
|
||||
.filter((t) => !!t);
|
||||
|
||||
await server.put("options", {
|
||||
openNoteContexts: JSON.stringify(openNoteContexts)
|
||||
});
|
||||
// Update the current window’s openNoteContexts in options
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const win = savedWindows.find((w: WindowState) => w.windowId === appContext.windowId);
|
||||
if (win) {
|
||||
win.contexts = openNoteContexts;
|
||||
} else {
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: openNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
});
|
||||
|
||||
appContext.addBeforeUnloadListener(this);
|
||||
@@ -69,8 +87,13 @@ export default class TabManager extends Component {
|
||||
}
|
||||
|
||||
async loadTabs() {
|
||||
// Get the current window’s openNoteContexts
|
||||
const savedWindows = options.getJson("openNoteContexts") || [];
|
||||
const currentWin = savedWindows.find(w => w.windowId === appContext.windowId);
|
||||
const openNoteContexts = currentWin ? currentWin.contexts : undefined;
|
||||
|
||||
try {
|
||||
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
|
||||
const noteContextsToOpen = openNoteContexts || [];
|
||||
|
||||
// preload all notes at once
|
||||
await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) =>
|
||||
@@ -119,6 +142,51 @@ export default class TabManager extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
// Save window contents
|
||||
if (currentWin as WindowState) {
|
||||
currentWin.createdAt = Date.now();
|
||||
currentWin.closedAt = 0;
|
||||
currentWin.contexts = filteredNoteContexts;
|
||||
} else {
|
||||
if (savedWindows?.length >= MAX_SAVED_WINDOWS) {
|
||||
// Filter out the oldest entry
|
||||
// 1) Never remove the "main" window
|
||||
// 2) Prefer removing the oldest closed window (closedAt !== 0)
|
||||
// 3) If no closed window exists, remove the window with the oldest created window
|
||||
let oldestClosedIndex = -1;
|
||||
let oldestClosedTime = Infinity;
|
||||
let oldestCreatedIndex = -1;
|
||||
let oldestCreatedTime = Infinity;
|
||||
savedWindows.forEach((w: WindowState, i: number) => {
|
||||
if (w.windowId === "main") return;
|
||||
if (w.closedAt !== 0) {
|
||||
if (w.closedAt < oldestClosedTime) {
|
||||
oldestClosedTime = w.closedAt;
|
||||
oldestClosedIndex = i;
|
||||
}
|
||||
} else {
|
||||
if (w.createdAt < oldestCreatedTime) {
|
||||
oldestCreatedTime = w.createdAt;
|
||||
oldestCreatedIndex = i;
|
||||
}
|
||||
}
|
||||
});
|
||||
const indexToRemove = oldestClosedIndex !== -1 ? oldestClosedIndex : oldestCreatedIndex;
|
||||
if (indexToRemove !== -1) {
|
||||
savedWindows.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
savedWindows.push({
|
||||
windowId: appContext.windowId,
|
||||
createdAt: Date.now(),
|
||||
closedAt: 0,
|
||||
contexts: filteredNoteContexts
|
||||
} as WindowState);
|
||||
}
|
||||
|
||||
await options.save("openNoteContexts", JSON.stringify(savedWindows));
|
||||
|
||||
// if there's a notePath in the URL, make sure it's open and active
|
||||
// (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
|
||||
if (parsedFromUrl.notePath) {
|
||||
|
||||
@@ -616,7 +616,9 @@ export default class FNote {
|
||||
}
|
||||
|
||||
isFolder() {
|
||||
return this.type === "search" || this.getFilteredChildBranches().length > 0;
|
||||
if (this.isLabelTruthy("subtreeHidden")) return false;
|
||||
if (this.type === "search") return true;
|
||||
return this.getFilteredChildBranches().length > 0;
|
||||
}
|
||||
|
||||
getFilteredChildBranches() {
|
||||
|
||||
29
apps/client/src/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover, interactive-widget=resizes-content" />
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||
<title>Trilium Notes</title>
|
||||
</head>
|
||||
|
||||
<body id="trilium-app">
|
||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||
|
||||
<!-- Required to match the PWA's top bar color with the theme -->
|
||||
<!-- This works even when the user directly changes --root-background in CSS -->
|
||||
<div id="background-color-tracker" style="position: absolute; visibility: hidden; color: var(--root-background); transition: color 1ms;"></div>
|
||||
|
||||
<script src="./index.ts" type="module"></script>
|
||||
|
||||
<!-- Required for correct loading of scripts in Electron -->
|
||||
<script>
|
||||
if (typeof module === 'object') {window.module = module; module = undefined;}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,111 @@
|
||||
async function bootstrap() {
|
||||
showSplash();
|
||||
await setupGlob();
|
||||
await Promise.all([
|
||||
initJQuery(),
|
||||
loadBootstrapCss()
|
||||
]);
|
||||
loadStylesheets();
|
||||
loadIcons();
|
||||
setBodyAttributes();
|
||||
await loadScripts();
|
||||
hideSplash();
|
||||
}
|
||||
|
||||
async function initJQuery() {
|
||||
const $ = (await import("jquery")).default;
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
}
|
||||
|
||||
async function setupGlob() {
|
||||
const response = await fetch(`/bootstrap${window.location.search}`);
|
||||
const json = await response.json();
|
||||
|
||||
window.global = globalThis; /* fixes https://github.com/webpack/webpack/issues/10035 */
|
||||
window.glob = {
|
||||
...json,
|
||||
activeDialog: null
|
||||
};
|
||||
}
|
||||
|
||||
async function loadBootstrapCss() {
|
||||
// We have to selectively import Bootstrap CSS based on text direction.
|
||||
if (glob.isRtl) {
|
||||
await import("bootstrap/dist/css/bootstrap.rtl.min.css");
|
||||
} else {
|
||||
await import("bootstrap/dist/css/bootstrap.min.css");
|
||||
}
|
||||
}
|
||||
|
||||
function loadStylesheets() {
|
||||
const { assetPath, themeCssUrl, themeUseNextAsBase } = window.glob;
|
||||
const cssToLoad: string[] = [];
|
||||
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`);
|
||||
|
||||
for (const href of cssToLoad) {
|
||||
const linkEl = document.createElement("link");
|
||||
linkEl.href = href;
|
||||
linkEl.rel = "stylesheet";
|
||||
document.head.appendChild(linkEl);
|
||||
}
|
||||
}
|
||||
|
||||
function loadIcons() {
|
||||
const styleEl = document.createElement("style");
|
||||
styleEl.innerText = window.glob.iconPackCss;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
function setBodyAttributes() {
|
||||
const { device, headingStyle, layoutOrientation, platform, isElectron, hasNativeTitleBar, hasBackgroundEffects, currentLocale, isMainWindow } = window.glob;
|
||||
const classesToSet = [
|
||||
device,
|
||||
`heading-style-${headingStyle}`,
|
||||
`layout-${layoutOrientation}`,
|
||||
`platform-${platform}`,
|
||||
isElectron && "electron",
|
||||
hasNativeTitleBar && "native-titlebar",
|
||||
hasBackgroundEffects && "background-effects",
|
||||
!isMainWindow && 'extra-window'
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
for (const classToSet of classesToSet) {
|
||||
document.body.classList.add(classToSet);
|
||||
}
|
||||
|
||||
document.body.lang = currentLocale.id;
|
||||
document.body.dir = currentLocale.rtl ? "rtl" : "ltr";
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
if (glob.device === "mobile") {
|
||||
await import("./mobile.js");
|
||||
} else {
|
||||
await import("./desktop.js");
|
||||
}
|
||||
}
|
||||
|
||||
function showSplash() {
|
||||
// hide body to reduce flickering on the startup. This is done through JS and not CSS to not hide <noscript>
|
||||
document.body.style.display = "none";
|
||||
}
|
||||
|
||||
function hideSplash() {
|
||||
document.body.style.display = "block";
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||
import treeService from "../services/tree.js";
|
||||
import froca from "../services/froca.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import type FAttachment from "../entities/fattachment.js";
|
||||
import attributes from "../services/attributes.js";
|
||||
import { executeBulkActions } from "../services/bulk_action.js";
|
||||
import clipboard from "../services/clipboard.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import noteCreateService from "../services/note_create.js";
|
||||
import noteTypesService from "../services/note_types.js";
|
||||
import server from "../services/server.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type FAttachment from "../entities/fattachment.js";
|
||||
import type { SelectMenuItemEventListener } from "../components/events.js";
|
||||
import treeService from "../services/tree.js";
|
||||
import utils from "../services/utils.js";
|
||||
import attributes from "../services/attributes.js";
|
||||
import { executeBulkActions } from "../services/bulk_action.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
|
||||
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
|
||||
|
||||
// TODO: Deduplicate once client/server is well split.
|
||||
interface ConvertToAttachmentResponse {
|
||||
@@ -72,6 +72,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
|
||||
|
||||
const notSearch = note?.type !== "search";
|
||||
const hasSubtreeHidden = note?.isLabelTruthy("subtreeHidden") ?? false;
|
||||
const isSpotlighted = this.node.extraClasses.includes("spotlighted-node");
|
||||
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
|
||||
const parentNotSearch = !parentNote || parentNote.type !== "search";
|
||||
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
|
||||
@@ -79,17 +81,18 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
const items: (MenuItem<TreeCommandNames> | null)[] = [
|
||||
{ title: t("tree-context-menu.open-in-a-new-tab"), command: "openInTab", shortcut: "Ctrl+Click", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-a-new-window"), command: "openNoteInWindow", uiIcon: "bx bx-window-open", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.open-in-popup"), command: "openNoteInPopup", uiIcon: "bx bx-edit", enabled: noSelectedNotes },
|
||||
|
||||
isHoisted
|
||||
? null
|
||||
: {
|
||||
title: `${t("tree-context-menu.hoist-note")}`,
|
||||
command: "toggleNoteHoisting",
|
||||
keyboardShortcut: "toggleNoteHoisting",
|
||||
uiIcon: "bx bxs-chevrons-up",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
title: `${t("tree-context-menu.hoist-note")}`,
|
||||
command: "toggleNoteHoisting",
|
||||
keyboardShortcut: "toggleNoteHoisting",
|
||||
uiIcon: "bx bxs-chevrons-up",
|
||||
enabled: noSelectedNotes && notSearch
|
||||
},
|
||||
!isHoisted || !isNotRoot
|
||||
? null
|
||||
: { title: t("tree-context-menu.unhoist-note"), command: "toggleNoteHoisting", keyboardShortcut: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
@@ -112,7 +115,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
keyboardShortcut: "createNoteInto",
|
||||
uiIcon: "bx bx-plus",
|
||||
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp,
|
||||
enabled: notSearch && noSelectedNotes && notOptionsOrHelp && !hasSubtreeHidden && !isSpotlighted,
|
||||
columns: 2
|
||||
},
|
||||
|
||||
@@ -150,8 +153,17 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ kind: "separator" },
|
||||
|
||||
{ title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
{ title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
!hasSubtreeHidden && { title: t("tree-context-menu.expand-subtree"), command: "expandSubtree", keyboardShortcut: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
|
||||
!hasSubtreeHidden && { title: t("tree-context-menu.collapse-subtree"), command: "collapseSubtree", keyboardShortcut: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
|
||||
{
|
||||
title: hasSubtreeHidden ? t("tree-context-menu.show-subtree") : t("tree-context-menu.hide-subtree"),
|
||||
uiIcon: "bx bx-show",
|
||||
handler: async () => {
|
||||
const note = await froca.getNote(this.node.data.noteId);
|
||||
if (!note) return;
|
||||
attributes.setBooleanWithInheritance(note, "subtreeHidden", !hasSubtreeHidden);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("tree-context-menu.sort-by"),
|
||||
command: "sortChildNotes",
|
||||
@@ -164,7 +176,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
|
||||
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptionsOrHelp }
|
||||
]
|
||||
].filter(Boolean) as MenuItem<TreeCommandNames>[]
|
||||
},
|
||||
|
||||
{ kind: "separator" },
|
||||
@@ -292,25 +304,30 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
target: "after",
|
||||
targetBranchId: this.node.data.branchId,
|
||||
type: type,
|
||||
isProtected: isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
type,
|
||||
isProtected,
|
||||
templateNoteId
|
||||
});
|
||||
} else if (command === "insertChildNote") {
|
||||
const parentNotePath = treeService.getNotePath(this.node);
|
||||
|
||||
noteCreateService.createNote(parentNotePath, {
|
||||
type: type,
|
||||
type,
|
||||
isProtected: this.node.data.isProtected,
|
||||
templateNoteId: templateNoteId
|
||||
templateNoteId
|
||||
});
|
||||
} else if (command === "openNoteInSplit") {
|
||||
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
|
||||
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
|
||||
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
|
||||
} else if (command === "openNoteInWindow") {
|
||||
appContext.triggerCommand("openInWindow", {
|
||||
notePath,
|
||||
hoistedNoteId: appContext.tabManager.getActiveContext()?.hoistedNoteId
|
||||
});
|
||||
} else if (command === "openNoteInPopup") {
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
|
||||
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
|
||||
} else if (command === "convertNoteToAttachment") {
|
||||
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
|
||||
return;
|
||||
@@ -332,11 +349,11 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
||||
|
||||
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
|
||||
} else if (command === "copyNotePathToClipboard") {
|
||||
navigator.clipboard.writeText("#" + notePath);
|
||||
navigator.clipboard.writeText(`#${ notePath}`);
|
||||
} else if (command) {
|
||||
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
|
||||
node: this.node,
|
||||
notePath: notePath,
|
||||
notePath,
|
||||
noteId: this.node.data.noteId,
|
||||
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
|
||||
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
|
||||
|
||||
139
apps/client/src/services/attributes.spec.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { buildNote } from "../test/easy-froca";
|
||||
import { setBooleanWithInheritance } from "./attributes";
|
||||
import froca from "./froca";
|
||||
import server from "./server.js";
|
||||
|
||||
// Spy on server methods to track calls
|
||||
// @ts-expect-error the generic typing is causing issues here
|
||||
server.put = vi.fn(async <T> (url: string, data?: T) => ({} as T));
|
||||
// @ts-expect-error the generic typing is causing issues here
|
||||
server.remove = vi.fn(async <T> (url: string) => ({} as T));
|
||||
|
||||
describe("Set boolean with inheritance", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("doesn't call server if value matches directly", async () => {
|
||||
const noteWithLabel = buildNote({
|
||||
title: "New note",
|
||||
"#foo": ""
|
||||
});
|
||||
const noteWithoutLabel = buildNote({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
await setBooleanWithInheritance(noteWithLabel, "foo", true);
|
||||
await setBooleanWithInheritance(noteWithoutLabel, "foo", false);
|
||||
expect(server.put).not.toHaveBeenCalled();
|
||||
expect(server.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets boolean normally without inheritance", async () => {
|
||||
const standaloneNote = buildNote({
|
||||
title: "New note"
|
||||
});
|
||||
|
||||
await setBooleanWithInheritance(standaloneNote, "foo", true);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("removes boolean normally without inheritance", async () => {
|
||||
const standaloneNote = buildNote({
|
||||
title: "New note",
|
||||
"#foo": ""
|
||||
});
|
||||
|
||||
const attributeId = standaloneNote.getLabel("foo")!.attributeId;
|
||||
await setBooleanWithInheritance(standaloneNote, "foo", false);
|
||||
expect(server.remove).toHaveBeenCalledWith(`notes/${standaloneNote.noteId}/attributes/${attributeId}`);
|
||||
});
|
||||
|
||||
it("doesn't call server if value matches inherited", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).not.toHaveBeenCalled();
|
||||
expect(server.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("overrides boolean with inheritance", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(true);
|
||||
await setBooleanWithInheritance(childNote, "foo", false);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "false",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("overrides boolean with inherited false", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "false",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note"
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).toHaveBeenCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes override boolean with inherited false with already existing value", async () => {
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
"#foo(inheritable)": "false",
|
||||
"children": [
|
||||
{
|
||||
title: "Child note",
|
||||
"#foo": "false",
|
||||
}
|
||||
]
|
||||
});
|
||||
const childNote = froca.getNoteFromCache(parentNote.children[0])!;
|
||||
expect(childNote.isLabelTruthy("foo")).toBe(false);
|
||||
await setBooleanWithInheritance(childNote, "foo", true);
|
||||
expect(server.put).toBeCalledWith(`notes/${childNote.noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: "foo",
|
||||
value: "",
|
||||
isInheritable: false
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
import server from "./server.js";
|
||||
import froca from "./froca.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import froca from "./froca.js";
|
||||
import type { AttributeRow } from "./load_results.js";
|
||||
import server from "./server.js";
|
||||
|
||||
async function addLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
@@ -16,8 +17,8 @@ async function addLabel(noteId: string, name: string, value: string = "", isInhe
|
||||
export async function setLabel(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "label",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
@@ -25,12 +26,42 @@ export async function setLabel(noteId: string, name: string, value: string = "",
|
||||
export async function setRelation(noteId: string, name: string, value: string = "", isInheritable = false) {
|
||||
await server.put(`notes/${noteId}/set-attribute`, {
|
||||
type: "relation",
|
||||
name: name,
|
||||
value: value,
|
||||
name,
|
||||
value,
|
||||
isInheritable
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a boolean label on the given note, taking inheritance into account. If the desired value matches the inherited
|
||||
* value, any owned label will be removed to allow the inherited value to take effect. If the desired value differs
|
||||
* from the inherited value, an owned label will be created or updated to reflect the desired value.
|
||||
*
|
||||
* When checking if the boolean value is set, don't use `note.hasLabel`; instead use `note.isLabelTruthy`.
|
||||
*
|
||||
* @param note the note on which to set the boolean label.
|
||||
* @param labelName the name of the label to set.
|
||||
* @param value the boolean value to set for the label.
|
||||
*/
|
||||
export async function setBooleanWithInheritance(note: FNote, labelName: string, value: boolean) {
|
||||
const actualValue = note.isLabelTruthy(labelName);
|
||||
if (actualValue === value) return;
|
||||
const hasInheritedValue = !note.hasOwnedLabel(labelName) && note.hasLabel(labelName);
|
||||
|
||||
if (hasInheritedValue) {
|
||||
if (value) {
|
||||
setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
// Label is inherited - override to false.
|
||||
setLabel(note.noteId, labelName, "false");
|
||||
}
|
||||
} else if (value) {
|
||||
setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
removeOwnedLabelByName(note, labelName);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAttributeById(noteId: string, attributeId: string) {
|
||||
await server.remove(`notes/${noteId}/attributes/${attributeId}`);
|
||||
}
|
||||
@@ -142,6 +173,7 @@ export default {
|
||||
setLabel,
|
||||
setRelation,
|
||||
setAttribute,
|
||||
setBooleanWithInheritance,
|
||||
removeAttributeById,
|
||||
removeOwnedLabelByName,
|
||||
removeOwnedRelationByName,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from "./i18n.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import server from "./server.js";
|
||||
import toastService, { type ToastOptionsWithRequiredId } from "./toast.js";
|
||||
import utils from "./utils.js";
|
||||
import ws from "./ws.js";
|
||||
|
||||
// TODO: Deduplicate type with server
|
||||
interface Response {
|
||||
@@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) {
|
||||
async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) {
|
||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||
if (!newParentBranch) {
|
||||
return;
|
||||
@@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st
|
||||
continue;
|
||||
}
|
||||
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`);
|
||||
const resp = await server.put<Response>(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(resp.message);
|
||||
|
||||
@@ -23,6 +23,12 @@ export interface RenderOptions {
|
||||
imageHasZoom?: boolean;
|
||||
/** If enabled, it will prevent the default behavior in which an empty note would display a list of children. */
|
||||
noChildrenList?: boolean;
|
||||
/** If enabled, it will prevent rendering of included notes. */
|
||||
noIncludedNotes?: boolean;
|
||||
/** If enabled, it will include archived notes when rendering children list. */
|
||||
includeArchivedNotes?: boolean;
|
||||
/** Set of note IDs that have already been seen during rendering to prevent infinite recursion. */
|
||||
seenNoteIds?: Set<string>;
|
||||
}
|
||||
|
||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||
|
||||
132
apps/client/src/services/content_renderer_text.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { trimIndentation } from "@triliumnext/commons";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildNote } from "../test/easy-froca";
|
||||
import renderText from "./content_renderer_text";
|
||||
|
||||
describe("Text content renderer", () => {
|
||||
it("renders included note", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const includedNote = buildNote({
|
||||
title: "Included note",
|
||||
content: "<p>This is the included note.</p>"
|
||||
});
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
|
||||
expect(contentEl.querySelectorAll("section.include-note p").length).toBe(1);
|
||||
});
|
||||
|
||||
it("skips rendering included note", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const includedNote = buildNote({
|
||||
title: "Included note",
|
||||
content: "<p>This is the included note.</p>"
|
||||
});
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="${includedNote.noteId}" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl), { noIncludedNotes: true });
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
|
||||
});
|
||||
|
||||
it("doesn't enter infinite loop on direct recursion", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const note = buildNote({
|
||||
title: "New note",
|
||||
id: "Y7mBwmRjQyb4",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
<section class="include-note" data-note-id="Y7mBwmRjQyb4" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(0);
|
||||
});
|
||||
|
||||
it("doesn't enter infinite loop on indirect recursion", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
buildNote({
|
||||
id: "first",
|
||||
title: "Included note",
|
||||
content: trimIndentation`\
|
||||
<p>This is the included note.</p>
|
||||
<section class="include-note" data-note-id="second" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
const note = buildNote({
|
||||
id: "second",
|
||||
title: "New note",
|
||||
content: trimIndentation`
|
||||
<p>
|
||||
Hi there
|
||||
</p>
|
||||
<section class="include-note" data-note-id="first" data-box-size="medium">
|
||||
|
||||
</section>
|
||||
`
|
||||
});
|
||||
await renderText(note, $(contentEl));
|
||||
expect(contentEl.querySelectorAll("section.include-note").length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders children list when note is empty", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
children: [
|
||||
{ title: "Child note 1" },
|
||||
{ title: "Child note 2" }
|
||||
]
|
||||
});
|
||||
await renderText(parentNote, $(contentEl));
|
||||
const items = contentEl.querySelectorAll("a");
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].textContent).toBe("Child note 1");
|
||||
expect(items[1].textContent).toBe("Child note 2");
|
||||
});
|
||||
|
||||
it("skips archived notes in children list", async () => {
|
||||
const contentEl = document.createElement("div");
|
||||
const parentNote = buildNote({
|
||||
title: "Parent note",
|
||||
children: [
|
||||
{ title: "Child note 1" },
|
||||
{ title: "Child note 2", "#archived": "" },
|
||||
{ title: "Child note 3" }
|
||||
]
|
||||
});
|
||||
await renderText(parentNote, $(contentEl));
|
||||
const items = contentEl.querySelectorAll("a");
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].textContent).toBe("Child note 1");
|
||||
expect(items[1].textContent).toBe("Child note 3");
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,14 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
|
||||
|
||||
if (blob && !isHtmlEmpty(blob.content)) {
|
||||
$renderedContent.append($('<div class="ck-content">').html(blob.content));
|
||||
await renderIncludedNotes($renderedContent[0]);
|
||||
|
||||
const seenNoteIds = options.seenNoteIds ?? new Set<string>();
|
||||
seenNoteIds.add("noteId" in note ? note.noteId : note.attachmentId);
|
||||
if (!options.noIncludedNotes) {
|
||||
await renderIncludedNotes($renderedContent[0], seenNoteIds);
|
||||
} else {
|
||||
$renderedContent.find("section.include-note").remove();
|
||||
}
|
||||
|
||||
if ($renderedContent.find("span.math-tex").length > 0) {
|
||||
renderMathInElement($renderedContent[0], { trust: true });
|
||||
@@ -35,11 +42,11 @@ export default async function renderText(note: FNote | FAttachment, $renderedCon
|
||||
await rewriteMermaidDiagramsInContainer($renderedContent[0] as HTMLDivElement);
|
||||
await formatCodeBlocks($renderedContent);
|
||||
} else if (note instanceof FNote && !options.noChildrenList) {
|
||||
await renderChildrenList($renderedContent, note);
|
||||
await renderChildrenList($renderedContent, note, options.includeArchivedNotes ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
async function renderIncludedNotes(contentEl: HTMLElement) {
|
||||
async function renderIncludedNotes(contentEl: HTMLElement, seenNoteIds: Set<string>) {
|
||||
// TODO: Consider duplicating with server's share/content_renderer.ts.
|
||||
const includeNoteEls = contentEl.querySelectorAll("section.include-note");
|
||||
|
||||
@@ -66,7 +73,15 @@ async function renderIncludedNotes(contentEl: HTMLElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedContent = (await content_renderer.getRenderedContent(note)).$renderedContent;
|
||||
if (seenNoteIds.has(noteId)) {
|
||||
console.warn(`Skipping inclusion of ${noteId} to avoid circular reference.`);
|
||||
includeNoteEl.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedContent = (await content_renderer.getRenderedContent(note, {
|
||||
seenNoteIds
|
||||
})).$renderedContent;
|
||||
includeNoteEl.replaceChildren(...renderedContent);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +113,7 @@ export async function applyInlineMermaid(container: HTMLDivElement) {
|
||||
}
|
||||
}
|
||||
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote) {
|
||||
async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: FNote, includeArchivedNotes: boolean) {
|
||||
let childNoteIds = note.getChildNoteIds();
|
||||
|
||||
if (!childNoteIds.length) {
|
||||
@@ -108,14 +123,16 @@ async function renderChildrenList($renderedContent: JQuery<HTMLElement>, note: F
|
||||
$renderedContent.css("padding", "10px");
|
||||
$renderedContent.addClass("text-with-ellipsis");
|
||||
|
||||
// just load the first 10 child notes
|
||||
if (childNoteIds.length > 10) {
|
||||
childNoteIds = childNoteIds.slice(0, 10);
|
||||
}
|
||||
|
||||
// just load the first 10 child notes
|
||||
const childNotes = await froca.getNotes(childNoteIds);
|
||||
|
||||
for (const childNote of childNotes) {
|
||||
if (childNote.isArchived && !includeArchivedNotes) continue;
|
||||
|
||||
$renderedContent.append(
|
||||
await link.createLink(`${note.noteId}/${childNote.noteId}`, {
|
||||
showTooltip: false,
|
||||
|
||||
@@ -27,10 +27,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
|
||||
loadResults.addRevision(ec.entityId, ec.noteId, ec.componentId);
|
||||
} else if (ec.entityName === "options") {
|
||||
const attributeEntity = ec.entity as FAttributeRow;
|
||||
if (attributeEntity.name === "openNoteContexts") {
|
||||
continue; // only noise
|
||||
}
|
||||
|
||||
options.set(attributeEntity.name as OptionNames, attributeEntity.value);
|
||||
loadResults.addOption(attributeEntity.name as OptionNames);
|
||||
} else if (ec.entityName === "attachments") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AttachmentRow, EtapiTokenRow, NoteType, OptionNames } from "@triliumnext/commons";
|
||||
|
||||
import type { AttributeType } from "../entities/fattribute.js";
|
||||
import type { EntityChange } from "../server_types.js";
|
||||
|
||||
@@ -135,7 +136,14 @@ export default class LoadResults {
|
||||
}
|
||||
|
||||
getBranchRows() {
|
||||
return this.branchRows.map((row) => this.getEntityRow("branches", row.branchId)).filter((branch) => !!branch);
|
||||
return this.branchRows.map((row) => {
|
||||
const branch = this.getEntityRow("branches", row.branchId);
|
||||
if (branch) {
|
||||
// Merge the componentId from the tracked row with the entity data
|
||||
return { ...branch, componentId: row.componentId };
|
||||
}
|
||||
return null;
|
||||
}).filter((branch) => !!branch) as BranchRow[];
|
||||
}
|
||||
|
||||
addNoteReordering(parentNoteId: string, componentId: string) {
|
||||
@@ -153,7 +161,14 @@ export default class LoadResults {
|
||||
getAttributeRows(componentId = "none"): AttributeRow[] {
|
||||
return this.attributeRows
|
||||
.filter((row) => row.componentId !== componentId)
|
||||
.map((row) => this.getEntityRow("attributes", row.attributeId))
|
||||
.map((row) => {
|
||||
const attr = this.getEntityRow("attributes", row.attributeId);
|
||||
if (attr) {
|
||||
// Merge the componentId from the tracked row with the entity data
|
||||
return { ...attr, componentId: row.componentId };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((attr) => !!attr) as AttributeRow[];
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,20 @@ describe("shortcuts", () => {
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match letter keys using code when key is a special character (macOS Alt behavior)", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' as the key, but code is still 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent("å", "KeyA");
|
||||
expect(keyMatches(macOSAltAEvent, "a")).toBe(true);
|
||||
|
||||
// Option + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent("˙", "KeyH");
|
||||
expect(keyMatches(macOSAltHEvent, "h")).toBe(true);
|
||||
|
||||
// Option + S produces 'ß'
|
||||
const macOSAltSEvent = createKeyboardEvent("ß", "KeyS");
|
||||
expect(keyMatches(macOSAltSEvent, "s")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesShortcut", () => {
|
||||
@@ -200,6 +214,33 @@ describe("shortcuts", () => {
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should match Alt+letter shortcuts on macOS where key is a special character", () => {
|
||||
// On macOS, pressing Option/Alt + A produces 'å' but code remains 'KeyA'
|
||||
const macOSAltAEvent = createKeyboardEvent({
|
||||
key: "å",
|
||||
code: "KeyA",
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSAltAEvent, "alt+a")).toBe(true);
|
||||
|
||||
// Option/Alt + H produces '˙'
|
||||
const macOSAltHEvent = createKeyboardEvent({
|
||||
key: "˙",
|
||||
code: "KeyH",
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSAltHEvent, "alt+h")).toBe(true);
|
||||
|
||||
// Combined with Ctrl: Ctrl+Alt+S where Alt produces 'ß'
|
||||
const macOSCtrlAltSEvent = createKeyboardEvent({
|
||||
key: "ß",
|
||||
code: "KeyS",
|
||||
ctrlKey: true,
|
||||
altKey: true
|
||||
});
|
||||
expect(matchesShortcut(macOSCtrlAltSEvent, "ctrl+alt+s")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindGlobalShortcut", () => {
|
||||
|
||||
@@ -213,8 +213,11 @@ export function keyMatches(e: KeyboardEvent, key: string): boolean {
|
||||
}
|
||||
|
||||
// For letter keys, use the physical key code for consistency
|
||||
// On macOS, Option/Alt key produces special characters, so we must use e.code
|
||||
if (key.length === 1 && key >= 'a' && key <= 'z') {
|
||||
return e.key.toLowerCase() === key.toLowerCase();
|
||||
// e.code is like "KeyA", "KeyB", etc.
|
||||
const expectedCode = `Key${key.toUpperCase()}`;
|
||||
return e.code === expectedCode || e.key.toLowerCase() === key.toLowerCase();
|
||||
}
|
||||
|
||||
// For regular keys, check both key and code as fallback
|
||||
|
||||
@@ -69,24 +69,6 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
});
|
||||
note.getBlob = async () => blob;
|
||||
|
||||
// Manage children.
|
||||
if (noteDef.children) {
|
||||
for (const childDef of noteDef.children) {
|
||||
const childNote = buildNote(childDef);
|
||||
const branchId = `${note.noteId}_${childNote.noteId}`;
|
||||
const branch = new FBranch(froca, {
|
||||
branchId,
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: note.noteId,
|
||||
notePosition: childNotePosition,
|
||||
fromSearchNote: false
|
||||
});
|
||||
froca.branches[branchId] = branch;
|
||||
note.addChild(childNote.noteId, branchId, false);
|
||||
childNotePosition += 10;
|
||||
}
|
||||
}
|
||||
|
||||
let position = 0;
|
||||
for (const [ key, value ] of Object.entries(noteDef)) {
|
||||
const attributeId = utils.randomString(12);
|
||||
@@ -136,5 +118,25 @@ export function buildNote(noteDef: NoteDefinition) {
|
||||
}
|
||||
noteAttributeCache.attributes[note.noteId].push(attribute);
|
||||
}
|
||||
|
||||
// Manage children.
|
||||
if (noteDef.children) {
|
||||
for (const childDef of noteDef.children) {
|
||||
const childNote = buildNote(childDef);
|
||||
const branchId = `${note.noteId}_${childNote.noteId}`;
|
||||
const branch = new FBranch(froca, {
|
||||
branchId,
|
||||
noteId: childNote.noteId,
|
||||
parentNoteId: note.noteId,
|
||||
notePosition: childNotePosition,
|
||||
fromSearchNote: false
|
||||
});
|
||||
froca.branches[branchId] = branch;
|
||||
note.addChild(childNote.noteId, branchId, false);
|
||||
childNote.addParent(note.noteId, branchId, false);
|
||||
childNotePosition += 10;
|
||||
}
|
||||
}
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ function injectGlobals() {
|
||||
uncheckedWindow.$ = $;
|
||||
uncheckedWindow.WebSocket = () => {};
|
||||
uncheckedWindow.glob = {
|
||||
isMainWindow: true
|
||||
isMainWindow: true,
|
||||
windowId: "main"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -798,7 +798,8 @@
|
||||
"expand_tooltip": "展开此集合的直接子代(单层深度)。点击右方箭头以查看更多选项。",
|
||||
"expand_first_level": "展开直接子代",
|
||||
"expand_nth_level": "展开 {{depth}} 层",
|
||||
"expand_all_levels": "展开所有层级"
|
||||
"expand_all_levels": "展开所有层级",
|
||||
"hide_child_notes": "隐藏树中的子笔记"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "今天还没有编辑过的笔记...",
|
||||
@@ -1505,7 +1506,10 @@
|
||||
"duplicate": "复制",
|
||||
"open-in-popup": "快速编辑",
|
||||
"archive": "归档",
|
||||
"unarchive": "解压"
|
||||
"unarchive": "解压",
|
||||
"open-in-a-new-window": "在新窗口中打开",
|
||||
"hide-subtree": "隐藏子树",
|
||||
"show-subtree": "显示子树"
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "访问 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a> 获取帮助。",
|
||||
@@ -1598,7 +1602,11 @@
|
||||
"shared-indicator-tooltip": "此笔记已公开分享",
|
||||
"shared-indicator-tooltip-with-url": "此笔记已公开分享至:{{- url}}",
|
||||
"clone-indicator-tooltip": "此笔记有 {{- count}} 个父级: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此笔记已克隆(1 个额外的父级:{{- parent}})"
|
||||
"clone-indicator-tooltip-single": "此笔记已克隆(1 个额外的父级:{{- parent}})",
|
||||
"subtree-hidden-tooltip_other": "从树中隐藏的 {{count}} 篇子笔记",
|
||||
"subtree-hidden-moved-title": "已添加到 {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "此集合隐藏其树中的子笔记。",
|
||||
"subtree-hidden-moved-description-other": "子笔记隐藏于此笔记的树中。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此窗口置顶"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "Über Trilium Notes",
|
||||
"title": "Über Trilium Notizen",
|
||||
"homepage": "Startseite:",
|
||||
"app_version": "App-Version:",
|
||||
"db_version": "DB-Version:",
|
||||
@@ -25,7 +25,13 @@
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Abruf der Liste von Widgets vom Server ist fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"open-script-note": "Script-Notiz öffnen",
|
||||
"widget-render-error": {
|
||||
"title": "Eine externe React Integration konnte nicht dargestellt werden"
|
||||
},
|
||||
"widget-missing-parent": "Der externen Integration fehlt die erforderliche Eigenschaft '{{property}}'\n\nFalls dieses Skript ohne UI-Element ausgeführt werden soll, benutze stattdessen '#run=frontendStartup'.",
|
||||
"scripting-error": "Benutzerdefinierter Skriptfehler: {{title}}"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Link hinzufügen",
|
||||
@@ -208,7 +214,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Infonachricht",
|
||||
"closeButton": "Schließen",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "In Zwischenablage kopieren"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_button": "Suche im Volltext",
|
||||
@@ -695,7 +702,9 @@
|
||||
"export_as_image": "Als Bild exportieren",
|
||||
"export_as_image_png": "PNG (Raster)",
|
||||
"export_as_image_svg": "SVG (Vektor)",
|
||||
"note_map": "Notizen Karte"
|
||||
"note_map": "Notizen Karte",
|
||||
"view_revisions": "Änderungshistorie...",
|
||||
"advanced": "Fortgeschritten"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Das Schaltflächen-Widget „{{componentId}}“ hat keinen definierten Klick-Handler"
|
||||
@@ -788,7 +797,8 @@
|
||||
"expand_all_levels": "Alle Ebenen erweitern",
|
||||
"expand_tooltip": "Erweitert die direkten Unterelemente dieser Sammlung (eine Ebene tiefer). Für weitere Optionen auf den Pfeil rechts klicken.",
|
||||
"expand_first_level": "Direkte Unterelemente erweitern",
|
||||
"expand_nth_level": "{{depth}} Ebenen erweitern"
|
||||
"expand_nth_level": "{{depth}} Ebenen erweitern",
|
||||
"hide_child_notes": "Unterknoten im Baum ausblenden"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "An diesem Tag wurden noch keine Notizen bearbeitet...",
|
||||
@@ -801,7 +811,7 @@
|
||||
"file_type": "Dateityp",
|
||||
"file_size": "Dateigröße",
|
||||
"download": "Herunterladen",
|
||||
"open": "Offen",
|
||||
"open": "Extern öffnen",
|
||||
"upload_new_revision": "Neue Revision hochladen",
|
||||
"upload_success": "Neue Dateirevision wurde hochgeladen.",
|
||||
"upload_failed": "Das Hochladen einer neuen Dateirevision ist fehlgeschlagen.",
|
||||
@@ -899,7 +909,7 @@
|
||||
"unknown_search_option": "Unbekannte Suchoption {{searchOptionName}}",
|
||||
"search_note_saved": "Suchnotiz wurde in {{-notePathTitle}} gespeichert",
|
||||
"actions_executed": "Aktionen wurden ausgeführt.",
|
||||
"view_options": "Anzeigeoptionen:"
|
||||
"view_options": "Optionen anzeigen:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Ähnliche Notizen",
|
||||
@@ -1005,9 +1015,9 @@
|
||||
"auto-detect-language": "Automatisch erkannt",
|
||||
"keeps-crashing": "Die Bearbeitungskomponente stürzt immer wieder ab. Bitte starten Sie Trilium neu. Wenn das Problem weiterhin besteht, erstellen Sie einen Fehlerbericht.",
|
||||
"editor_crashed_title": "Der Text Editor ist abgestürzt",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber einzelne Ihrer letzten Änderungen waren möglicherweise noch nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Zeige mehr Details…",
|
||||
"editor_crashed_details_intro": "Falls Sie diesen Fehler mehrmals sehen, melden Sie dies auf GitHub mit den folgenden Informationen.",
|
||||
"editor_crashed_content": "Ihr Inhalt wurde erfolgreich wiederhergestellt, aber kürzlich gemachte Änderungen wurden unter Umständen nicht gespeichert.",
|
||||
"editor_crashed_details_button": "Mehr Details anzeigen...",
|
||||
"editor_crashed_details_intro": "Falls dieser Fehler häufiger auftritt, ziehen Sie in Betracht uns diesen über GitHub zu melden, indem Sie die folgenden Informationen bereitstellen.",
|
||||
"editor_crashed_details_title": "Technische Informationen"
|
||||
},
|
||||
"empty": {
|
||||
@@ -1408,7 +1418,7 @@
|
||||
"will_be_deleted_in": "Dieser Anhang wird in {{time}} automatisch gelöscht",
|
||||
"will_be_deleted_soon": "Dieser Anhang wird bald automatisch gelöscht",
|
||||
"deletion_reason": ", da der Anhang nicht im Inhalt der Notiz verlinkt ist. Um das Löschen zu verhindern, füge den Anhangslink wieder in den Inhalt ein oder wandel den Anhang in eine Notiz um.",
|
||||
"role_and_size": "Rolle: {{role}}, Größe: {{size}}",
|
||||
"role_and_size": "Rolle: {{role}}, Größe: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Anhangslink in die Zwischenablage kopiert.",
|
||||
"unrecognized_role": "Unbekannte Anhangsrolle „{{role}}“."
|
||||
},
|
||||
@@ -1459,10 +1469,13 @@
|
||||
"import-into-note": "In Notiz importieren",
|
||||
"apply-bulk-actions": "Massenaktionen anwenden",
|
||||
"converted-to-attachments": "{{count}} Notizen wurden als Anhang konvertiert.",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest?",
|
||||
"convert-to-attachment-confirm": "Bist du sicher, dass du die ausgewählten Notizen in Anhänge ihrer übergeordneten Notizen umwandeln möchtest? Diese Operation wird nur auf Bildnotizes angewandt. Andere Notizen werden übersprungen.",
|
||||
"open-in-popup": "Schnellbearbeitung",
|
||||
"archive": "Archiviere",
|
||||
"unarchive": "Entarchivieren"
|
||||
"unarchive": "Entarchivieren",
|
||||
"open-in-a-new-window": "In neuem Fenster öffnen",
|
||||
"hide-subtree": "Teilbaum ausblenden",
|
||||
"show-subtree": "Teilbaum anzeigen"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Diese Notiz ist öffentlich geteilt auf {{- link}}.",
|
||||
@@ -1552,7 +1565,16 @@
|
||||
"create-child-note": "Unternotiz anlegen",
|
||||
"unhoist": "Fokus verlassen",
|
||||
"toggle-sidebar": "Seitenleiste ein-/ausblenden",
|
||||
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig."
|
||||
"dropping-not-allowed": "Ablegen von Notizen an dieser Stelle ist nicht zulässig.",
|
||||
"clone-indicator-tooltip": "Diese Notiz hat {{- count}} Elterknoten: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Diese Notiz ist geklont (1 weiterer Elternknoten: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Diese Notiz ist öffentlich einsehbar",
|
||||
"shared-indicator-tooltip-with-url": "Diese Notiz ist unter {{- url}} öffentlich einsehbar",
|
||||
"subtree-hidden-tooltip_one": "{{count}} Unterknoten, der im Baum ausgeblendet ist",
|
||||
"subtree-hidden-tooltip_other": "{{count}} Unterknoten, die im Baum ausgeblendet sind",
|
||||
"subtree-hidden-moved-title": "Zu {{title}} hinzugefügt",
|
||||
"subtree-hidden-moved-description-collection": "Diese Sammlung blendet ihre Unternotizem im Baum aus.",
|
||||
"subtree-hidden-moved-description-other": "Diese Sammlung blendet ihre Unterknoten im Baum aus."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Dieses Fenster immer oben halten"
|
||||
@@ -1563,7 +1585,9 @@
|
||||
"printing_pdf": "PDF-Export läuft…",
|
||||
"print_report_title": "Druckreport",
|
||||
"print_report_collection_details_button": "Details anzeigen",
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen"
|
||||
"print_report_collection_details_ignored_notes": "Ignorierte Notizen",
|
||||
"print_report_collection_content_one": "{{count}} Notiz in der Sammlung konnte nicht gedruckt werden, weil sie nicht unterstützt ist oder geschützt ist.",
|
||||
"print_report_collection_content_other": "{{count}} Notizen in der Sammlung konnten nicht gedruckt werden, weil sie nicht unterstützt sind oder geschützt sind."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "Titel der Notiz hier eingeben…",
|
||||
@@ -1716,7 +1740,8 @@
|
||||
"open_note_in_new_tab": "Notiz in neuen Tab öffnen",
|
||||
"open_note_in_new_split": "Notiz in neuen geteilten Tab öffnen",
|
||||
"open_note_in_new_window": "Notiz in neuen Fenster öffnen",
|
||||
"open_note_in_popup": "Schnellbearbeitung"
|
||||
"open_note_in_popup": "Schnellbearbeitung",
|
||||
"open_note_in_other_split": "Notiz in neuer Spalte öffnen"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Desktop Anwendung",
|
||||
@@ -1984,8 +2009,9 @@
|
||||
"unknown_widget": "Unbekanntes Widget für '{{id}}'."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Nicht gesetzt",
|
||||
"configure-languages": "Konfiguriere Sprachen..."
|
||||
"not_set": "Keine Sprache ausgewählt",
|
||||
"configure-languages": "Konfiguriere Sprachen...",
|
||||
"help-on-languages": "Zu Übersetzungen beitragen..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Inhaltssprachen",
|
||||
@@ -2003,7 +2029,8 @@
|
||||
"button_title": "Exportiere Diagramm als PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden."
|
||||
"export_to_png": "Das Diagramm konnte als PNG nicht exportiert werden.",
|
||||
"export_to_svg": "Das Diagramm konnte nicht als SVG exportiert werden."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Aussehen",
|
||||
@@ -2051,7 +2078,7 @@
|
||||
"book_properties_config": {
|
||||
"hide-weekends": "Wochenenden ausblenden",
|
||||
"display-week-numbers": "Zeige Kalenderwoche",
|
||||
"map-style": "Kartenstil:",
|
||||
"map-style": "Kartenstil",
|
||||
"max-nesting-depth": "Maximale Verschachtelungstiefe:",
|
||||
"raster": "Raster",
|
||||
"vector_light": "Vektor (Hell)",
|
||||
@@ -2104,14 +2131,20 @@
|
||||
"background_effects_title": "Hintergrundeffekte sind jetzt zuverlässig nutzbar",
|
||||
"background_effects_message": "Auf Windows-Geräten sind die Hintergrundeffekte nun vollständig stabil. Die Hintergrundeffekte verleihen der Benutzeroberfläche einen Farbakzent, indem der Hintergrund dahinter weichgezeichnet wird. Diese Technik wird auch in anderen Anwendungen wie dem Windows-Explorer eingesetzt.",
|
||||
"background_effects_button": "Aktiviere Hintergrundeffekte",
|
||||
"dismiss": "Ablehnen"
|
||||
"dismiss": "Ablehnen",
|
||||
"new_layout_title": "Neues Layout",
|
||||
"new_layout_message": "Wir haben ein modernisiertes Layout für Trilium eingeführt. Die Multifunktionsleiste wurde entfernt und als neue Statusanzeige und ausklappbaren Sektionen (wie hervorgehobenen Attributen), welche Schlüsselfunktionen übernehmen, nahtlos in das Hauptinterface integriert.\n\nDas neue Layout ist standardmäßig aktiviert und kann temporär in Optionen → Anzeige deaktiviert werden.",
|
||||
"new_layout_button": "Mehr Informationen"
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Ähnliche Einstellungen"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Farbschema für Code-Blöcke in Textnotizen",
|
||||
"related_code_notes": "Farbschema für Code-Notizen"
|
||||
"related_code_notes": "Farbschema für Code-Notizen",
|
||||
"ui": "Benutzeroberfläche",
|
||||
"ui_old_layout": "Altes Layout",
|
||||
"ui_new_layout": "Neues Layout"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
@@ -2147,6 +2180,88 @@
|
||||
"experimental_features": {
|
||||
"title": "Experimentelle Optionen",
|
||||
"disclaimer": "Diese Optionen sind experimentell und können Instabilitäten verursachen. Achtsam zu verwenden.",
|
||||
"new_layout_name": "Neues Layout"
|
||||
"new_layout_name": "Neues Layout",
|
||||
"new_layout_description": "Probiere das neue Layout für eine modernere Darstellung und verbesserte Benutzbarkeit aus. Kann sich in Zukunft stark ändern."
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Bei der Kommunikation mit dem Server ist ein Fehler aufgetreten",
|
||||
"unknown_http_error_content": "Statuscode: {{statusCode}}\nURL: {{method}} {{url}}\nNachricht: {{message}}",
|
||||
"traefik_blocks_requests": "Der Traefik Reverse-Proxy hat ein fatales Update bekommen, welche die Kommunikation mit dem Server stört."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Zur vorherigen Notiz zurück kehren",
|
||||
"go-forward": "Zur nächsten Notiz"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "Gehoben",
|
||||
"hoisted_badge_title": "Abgesenkt",
|
||||
"workspace_badge": "Arbeitsfläche",
|
||||
"scroll_to_top_title": "Zum Anfang der Notiz springen",
|
||||
"create_new_note": "Neue Unternotiz erstellen",
|
||||
"empty_hide_archived_notes": "Archivierte Notizen ausblenden"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Nicht Änderbar",
|
||||
"read_only_explicit_description": "Diese Notiz wurde händisch als nicht änderbar markiert.\nKlicke hier um sie temporär zu bearbeiten.",
|
||||
"read_only_auto": "Automatisch nicht änderbar",
|
||||
"read_only_auto_description": "Diese Notiz wurde automatisch aus Leistungsgründen als nicht änderbar markiert. Dieses automatische Limit kann in den Einstellungen angepasst werden.\n\nKlicke hier, um sie temporär zu bearbeiten.",
|
||||
"read_only_temporarily_disabled": "Temporär bearbeitbar",
|
||||
"read_only_temporarily_disabled_description": "Diese Notiz ist aktuell bearbeitbar, ist aber normalerweise nicht änderbar. Sobald du zu einer anderen Notiz navigierst, kehrt diese Notiz in ihren Normalzustand zurück.\n\nKlicke hier, um die Notiz wieder nicht änderbar zu machen.",
|
||||
"shared_publicly": "Öffentlich geteilt",
|
||||
"shared_locally": "Lokal geteilt",
|
||||
"shared_copy_to_clipboard": "Link in die Zwischenablage kopieren",
|
||||
"shared_open_in_browser": "Link öffnen",
|
||||
"shared_unshare": "Teilen aufheben",
|
||||
"clipped_note": "Internetschnellverweis",
|
||||
"clipped_note_description": "Diese Notiz wurde von {{url}} übernommen.\n\nKlicke hier, um zum Ursprung zu gehen.",
|
||||
"execute_script": "Skript ausführen",
|
||||
"execute_script_description": "Diese Notiz ist eine Skriptnotiz. Klicke hier, um das Skript auszuführen.",
|
||||
"execute_sql": "SQL ausführen",
|
||||
"execute_sql_description": "Diese Notiz ist eine SQL-Notiz. Klicke hier, um die SQL-Abfrage auszuführen.",
|
||||
"save_status_saved": "Gespeichert",
|
||||
"save_status_saving": "Speichern...",
|
||||
"save_status_unsaved": "Nicht gespeichert",
|
||||
"save_status_error": "Speichern fehlgeschlagen",
|
||||
"save_status_saving_tooltip": "Änderungen werden gespeichert.",
|
||||
"save_status_unsaved_tooltip": "Es gibt ungespeicherte Änderungen, welche gleich automatisch gespeichert werden.",
|
||||
"save_status_error_tooltip": "Beim speichern der Notiz ist ein Fehler aufgetreten. Wenn möglich, versuche die Notiz woandershin zu kopieren und die Applikation neu zu laden."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Inhaltssprache ändern",
|
||||
"note_info_title": "Notizinfo anzeigen (z.B.: Datum, Notizgröße)",
|
||||
"backlinks_one": "{{count}} Rücklink",
|
||||
"backlinks_other": "{{count}} Rücklinks",
|
||||
"backlinks_title_one": "Rücklink anzeigen",
|
||||
"backlinks_title_other": "Rücklinks anzeigen",
|
||||
"attachments_one": "{{count}} Anhang",
|
||||
"attachments_other": "{{count}} Anhänge",
|
||||
"attachments_title_one": "Anhang in einem neuen Tab öffnen",
|
||||
"attachments_title_other": "Anhänge in einem neuen Tab öffnen",
|
||||
"attributes_one": "{{count}} Eigenschaft",
|
||||
"attributes_other": "{{count}} Eigenschaften",
|
||||
"attributes_title": "Eigene und gererbte Eigenschaften",
|
||||
"note_paths_one": "{{count}} Pfad",
|
||||
"note_paths_other": "{{count}} Pfade",
|
||||
"note_paths_title": "Notizpfade",
|
||||
"code_note_switcher": "Sprachmodus ändern"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Notizeigenschaften"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Für diese Notiz gibt es nichts anzuzeigen",
|
||||
"empty_button": "Anzeige ausblenden",
|
||||
"toggle": "Rechte Anzeige umschalten",
|
||||
"custom_widget_go_to_source": "Zum Ursprungscode"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} Anhang",
|
||||
"attachments_other": "{{count}} Anhänge",
|
||||
"layers_one": "{{count}} Ebene",
|
||||
"layers_other": "{{count}} Ebenen",
|
||||
"pages_one": "{{count}} Seite",
|
||||
"pages_other": "{{count}} Seiten",
|
||||
"pages_alt": "Seite {{pageNumber}}",
|
||||
"pages_loading": "Laden..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,5 +69,8 @@
|
||||
"clear-color": "Clear note colour",
|
||||
"set-color": "Set note colour",
|
||||
"set-custom-color": "Set custom note colour"
|
||||
},
|
||||
"about": {
|
||||
"title": "About Trilium Notes"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,7 +800,8 @@
|
||||
"geo-map": "Geo Map",
|
||||
"board": "Board",
|
||||
"presentation": "Presentation",
|
||||
"include_archived_notes": "Show archived notes"
|
||||
"include_archived_notes": "Show archived notes",
|
||||
"hide_child_notes": "Hide child notes in tree"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "No edited notes on this day yet...",
|
||||
@@ -1643,6 +1644,7 @@
|
||||
"tree-context-menu": {
|
||||
"open-in-a-new-tab": "Open in a new tab",
|
||||
"open-in-a-new-split": "Open in a new split",
|
||||
"open-in-a-new-window": "Open in a new window",
|
||||
"insert-note-after": "Insert note after",
|
||||
"insert-child-note": "Insert child note",
|
||||
"archive": "Archive",
|
||||
@@ -1655,6 +1657,8 @@
|
||||
"advanced": "Advanced",
|
||||
"expand-subtree": "Expand subtree",
|
||||
"collapse-subtree": "Collapse subtree",
|
||||
"hide-subtree": "Hide subtree",
|
||||
"show-subtree": "Show subtree",
|
||||
"sort-by": "Sort by...",
|
||||
"recent-changes-in-subtree": "Recent changes in subtree",
|
||||
"convert-to-attachment": "Convert to attachment",
|
||||
@@ -1772,7 +1776,12 @@
|
||||
"clone-indicator-tooltip": "This note has {{- count}} parents: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "This note is cloned (1 additional parent: {{- parent}})",
|
||||
"shared-indicator-tooltip": "This note is shared publicly",
|
||||
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}"
|
||||
"shared-indicator-tooltip-with-url": "This note is shared publicly at: {{- url}}",
|
||||
"subtree-hidden-tooltip_one": "{{count}} child note that is hidden from the tree",
|
||||
"subtree-hidden-tooltip_other": "{{count}} child notes that are hidden from the tree",
|
||||
"subtree-hidden-moved-title": "Added to {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "This collection hides its child notes in the tree.",
|
||||
"subtree-hidden-moved-description-other": "Child notes are hidden in the tree for this note."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Hubo un fallo al cargar un script personalizado",
|
||||
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
|
||||
"message": "El script no pudo ser ejecutado debido a:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Hubo un fallo al obtener la lista de widgets del servidor"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Hubo un fallo al renderizar un widget personalizado de React"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -162,7 +168,8 @@
|
||||
"other": "Otro",
|
||||
"quickSearch": "centrarse en la entrada de búsqueda rápida",
|
||||
"inPageSearch": "búsqueda en la página",
|
||||
"title": "Hoja de ayuda"
|
||||
"title": "Hoja de ayuda",
|
||||
"editShortcuts": "Editar atajos de teclado"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar a nota",
|
||||
@@ -690,7 +697,7 @@
|
||||
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
|
||||
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?",
|
||||
"print_pdf": "Exportar como PDF...",
|
||||
"open_note_on_server": "Abrir nota en el servidor"
|
||||
"open_note_on_server": "Abrir nota en servidor"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
|
||||
@@ -736,7 +743,7 @@
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relación",
|
||||
"backlink_one": "{{count}} Vínculo de retroceso",
|
||||
"backlink_many": "",
|
||||
"backlink_many": "{{count}} Vínculos de retroceso",
|
||||
"backlink_other": "{{count}} vínculos de retroceso"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
@@ -749,7 +756,10 @@
|
||||
"note_icon": {
|
||||
"change_note_icon": "Cambiar icono de nota",
|
||||
"search": "Búsqueda:",
|
||||
"reset-default": "Restablecer a icono por defecto"
|
||||
"reset-default": "Restablecer a icono por defecto",
|
||||
"search_placeholder_one": "Buscar {{number}} icono a través de {{count}} paquetes",
|
||||
"search_placeholder_many": "Buscar {{number}} iconos a través de {{count}} paquetes",
|
||||
"search_placeholder_other": "Buscar {{number}} iconos a través de {{count}} paquetes"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo de nota",
|
||||
@@ -789,7 +799,7 @@
|
||||
"file_type": "Tipo de archivo",
|
||||
"file_size": "Tamaño del archivo",
|
||||
"download": "Descargar",
|
||||
"open": "Abrir",
|
||||
"open": "Abrir externamente",
|
||||
"upload_new_revision": "Subir nueva revisión",
|
||||
"upload_success": "Se ha subido una nueva revisión de archivo.",
|
||||
"upload_failed": "Error al cargar una nueva revisión de archivo.",
|
||||
@@ -1302,11 +1312,11 @@
|
||||
"code_mime_types": {
|
||||
"title": "Tipos MIME disponibles en el menú desplegable",
|
||||
"tooltip_syntax_highlighting": "Resaltado de sintaxis",
|
||||
"tooltip_code_block_syntax": "Bloques de código en notas de texto",
|
||||
"tooltip_code_note_syntax": "Notas de código"
|
||||
"tooltip_code_block_syntax": "Bloques de Código en notas de Texto",
|
||||
"tooltip_code_note_syntax": "Notas de Código"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Atajos de teclas de Vim",
|
||||
"use_vim_keybindings_in_code_notes": "Combinaciones de teclas Vim",
|
||||
"enable_vim_keybindings": "Habilitar los atajos de teclas de Vim en la notas de código (no es modo ex)"
|
||||
},
|
||||
"wrap_lines": {
|
||||
@@ -1571,7 +1581,7 @@
|
||||
"will_be_deleted_in": "Este archivo adjunto se eliminará automáticamente en {{time}}",
|
||||
"will_be_deleted_soon": "Este archivo adjunto se eliminará automáticamente pronto",
|
||||
"deletion_reason": ", porque el archivo adjunto no está vinculado en el contenido de la nota. Para evitar la eliminación, vuelva a agregar el enlace del archivo adjunto al contenido o convierta el archivo adjunto en una nota.",
|
||||
"role_and_size": "Rol: {{role}}, Tamaño: {{size}}",
|
||||
"role_and_size": "Rol: {{role}}, tamaño: {{size}}, MIME: {{- mimeType}}",
|
||||
"link_copied": "Enlace del archivo adjunto copiado al portapapeles.",
|
||||
"unrecognized_role": "Rol de archivo adjunto no reconocido '{{role}}'."
|
||||
},
|
||||
@@ -1622,7 +1632,7 @@
|
||||
"import-into-note": "Importar a nota",
|
||||
"apply-bulk-actions": "Aplicar acciones en lote",
|
||||
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
|
||||
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?",
|
||||
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres? Esta operación solo aplica a notas de Imagen, otras notas serán omitidas.",
|
||||
"open-in-popup": "Edición rápida",
|
||||
"archive": "Archivar",
|
||||
"unarchive": "Desarchivar"
|
||||
@@ -1717,7 +1727,10 @@
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "No se pudo encontrar typeWidget para el tipo '{{type}}'",
|
||||
"printing": "Impresión en curso...",
|
||||
"printing_pdf": "Exportando a PDF en curso.."
|
||||
"printing_pdf": "Exportando a PDF en curso..",
|
||||
"print_report_collection_content_one": "{{count}} nota en la colección no se puede imprimir porque no son compatibles o está protegida.",
|
||||
"print_report_collection_content_many": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas.",
|
||||
"print_report_collection_content_other": "{{count}} notas en la colección no se pueden imprimir porque no son compatibles o están protegidas."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "escriba el título de la nota aquí..."
|
||||
@@ -1929,7 +1942,7 @@
|
||||
"unknown_widget": "Widget desconocido para \"{{id}}\"."
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "No establecido",
|
||||
"not_set": "Idioma no establecido",
|
||||
"configure-languages": "Configurar idiomas..."
|
||||
},
|
||||
"content_language": {
|
||||
@@ -1968,7 +1981,7 @@
|
||||
"hide-weekends": "Ocultar fines de semana",
|
||||
"show-scale": "Mostrar escala",
|
||||
"display-week-numbers": "Mostrar números de semana",
|
||||
"map-style": "Estilo de mapa:",
|
||||
"map-style": "Estilo de mapa",
|
||||
"max-nesting-depth": "Máxima profundidad de anidamiento:",
|
||||
"vector_light": "Vector (claro)",
|
||||
"vector_dark": "Vector (oscuro)",
|
||||
@@ -2097,5 +2110,36 @@
|
||||
"clear-color": "Borrar color de nota",
|
||||
"set-color": "Asignar color de nota",
|
||||
"set-custom-color": "Asignar color de nota personalizado"
|
||||
},
|
||||
"status_bar": {
|
||||
"backlinks_one": "{{count}} vínculo de retroceso",
|
||||
"backlinks_many": "{{count}} vínculos de retroceso",
|
||||
"backlinks_other": "{{count}} vínculos de retroceso",
|
||||
"backlinks_title_one": "Ver vínculo de retroceso",
|
||||
"backlinks_title_many": "Ver vínculos de retroceso",
|
||||
"backlinks_title_other": "Ver vínculos de retroceso",
|
||||
"attachments_one": "{{count}} adjunto",
|
||||
"attachments_many": "{{count}} adjuntos",
|
||||
"attachments_other": "{{count}} adjuntos",
|
||||
"attachments_title_one": "Ver adjunto en una nueva pestaña",
|
||||
"attachments_title_many": "Ver adjuntos en una nueva pestaña",
|
||||
"attachments_title_other": "Ver adjuntos en una nueva pestaña",
|
||||
"attributes_one": "{{count}} atributo",
|
||||
"attributes_many": "{{count}} atributos",
|
||||
"attributes_other": "{{count}} atributos",
|
||||
"note_paths_one": "{{count}} ruta",
|
||||
"note_paths_many": "{{count}} rutas",
|
||||
"note_paths_other": "{{count}} rutas"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} adjunto",
|
||||
"attachments_many": "{{count}} adjuntos",
|
||||
"attachments_other": "{{count}} adjuntos",
|
||||
"layers_one": "{{count}} capa",
|
||||
"layers_many": "{{count}} capas",
|
||||
"layers_other": "{{count}} capas",
|
||||
"pages_one": "{{count}} página",
|
||||
"pages_many": "{{count}} páginas",
|
||||
"pages_other": "{{count}} páginas"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Echec du chargement d'un script personnalisé",
|
||||
"message": "Le script de la note avec l'ID \"{{id}}\", intitulé \"{{title}}\" n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
"message": "Le script n'a pas pu être exécuté à cause de\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Impossible d'obtenir la liste des widgets depuis le serveur"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"about": {
|
||||
"title": "ट्रिलियम नोट्स के बारें में",
|
||||
"build_date": "निर्माण की तारीख:"
|
||||
"build_date": "निर्माण की तारीख:",
|
||||
"app_version": "ऐप वर्ज़न:",
|
||||
"db_version": "DB वर्ज़न:",
|
||||
"build_revision": "बिल्ड रिविज़न:"
|
||||
},
|
||||
"toast": {
|
||||
"widget-error": {
|
||||
@@ -31,5 +34,17 @@
|
||||
},
|
||||
"add_link": {
|
||||
"note": "नोट"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"other": "अन्य"
|
||||
},
|
||||
"clone_to": {
|
||||
"search_for_note_by_its_name": "नोट क नाम से नोट खोजें"
|
||||
},
|
||||
"confirm": {
|
||||
"also_delete_note": "नोट भी डिलीट करें"
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "नोट्स प्रिव्यू डिलीट करें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Nem sikerült betölteni az egyéni szkriptet",
|
||||
"message": "A(z) \"{{id}}\" azonosítójú, \"{{title}}\" című jegyzetből származó szkript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
"message": "A skript nem hajtható végre a következő ok miatt:\n\n{{message}}"
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "A Widget-ek letöltése sikertelen volt"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "Nem sikerült renderelni a React widget-et"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
|
||||
@@ -1895,7 +1895,11 @@
|
||||
"create-child-note": "Crea nota figlio",
|
||||
"unhoist": "Sganciare",
|
||||
"toggle-sidebar": "Attiva/disattiva la barra laterale",
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione."
|
||||
"dropping-not-allowed": "Non è consentito lasciare appunti in questa posizione.",
|
||||
"clone-indicator-tooltip": "Questa nota ha {{- count}} genitori: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "Questa nota è stata clonata (1 genitore aggiuntivo: {{- parent}})",
|
||||
"shared-indicator-tooltip": "Questa nota è condivisa pubblicamente",
|
||||
"shared-indicator-tooltip-with-url": "Questa nota è condivisa pubblicamente all'indirizzo: {{- url}}"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Mantieni la finestra in primo piano"
|
||||
@@ -2200,7 +2204,14 @@
|
||||
"execute_sql_description": "Questa nota è una nota SQL. Clicca per eseguire la query SQL.",
|
||||
"shared_copy_to_clipboard": "Copia link negli appunti",
|
||||
"shared_open_in_browser": "Apri il link nel browser",
|
||||
"shared_unshare": "Rimuovi condivisione"
|
||||
"shared_unshare": "Rimuovi condivisione",
|
||||
"save_status_saved": "Salvato",
|
||||
"save_status_saving": "Salvataggio in corso...",
|
||||
"save_status_unsaved": "Non salvato",
|
||||
"save_status_error": "Salvataggio non riuscito",
|
||||
"save_status_saving_tooltip": "Le modifiche sono state salvate.",
|
||||
"save_status_unsaved_tooltip": "Ci sono modifiche non salvate. Verranno salvate automaticamente tra un attimo.",
|
||||
"save_status_error_tooltip": "Si è verificato un errore durante il salvataggio della nota. Se possibile, prova a copiare il contenuto della nota altrove e a ricaricare l'applicazione."
|
||||
},
|
||||
"breadcrumb": {
|
||||
"workspace_badge": "Area di lavoro",
|
||||
@@ -2243,5 +2254,18 @@
|
||||
"empty_button": "Nascondi il pannello",
|
||||
"toggle": "Attiva/disattiva pannello destro",
|
||||
"custom_widget_go_to_source": "Vai al codice sorgente"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} allegato",
|
||||
"attachments_many": "{{count}} allegati",
|
||||
"attachments_other": "{{count}} allegati",
|
||||
"layers_one": "{{count}} livello",
|
||||
"layers_many": "{{count}} livelli",
|
||||
"layers_other": "{{count}} livelli",
|
||||
"pages_one": "{{count}} pagina",
|
||||
"pages_many": "{{count}} pagine",
|
||||
"pages_other": "{{count}} pagine",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Caricamento in corso..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,10 @@
|
||||
"unhoist-note": "ノートのホイストを解除",
|
||||
"edit-branch-prefix": "ブランチの接頭辞を編集",
|
||||
"archive": "アーカイブ",
|
||||
"unarchive": "アーカイブ解除"
|
||||
"unarchive": "アーカイブ解除",
|
||||
"open-in-a-new-window": "新しいウィンドウで開く",
|
||||
"hide-subtree": "サブツリーを非表示",
|
||||
"show-subtree": "サブツリーを表示"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "禅モードを退出"
|
||||
@@ -568,7 +571,8 @@
|
||||
"expand_tooltip": "このコレクションの直下の子(1階層下)を展開します。その他のオプションについては、右側の矢印を押してください。",
|
||||
"expand_first_level": "直下の子を展開",
|
||||
"expand_nth_level": "{{depth}} 階層下まで展開",
|
||||
"expand_all_levels": "すべての階層を展開"
|
||||
"expand_all_levels": "すべての階層を展開",
|
||||
"hide_child_notes": "ツリー内の子ノートを非表示"
|
||||
},
|
||||
"note_types": {
|
||||
"geo-map": "ジオマップ",
|
||||
@@ -1248,7 +1252,11 @@
|
||||
"clone-indicator-tooltip": "このノートには {{- count}} 個の親があります: {{- parents}}",
|
||||
"clone-indicator-tooltip-single": "このノートは複製されています (親が 1 件追加: {{- parent}})",
|
||||
"shared-indicator-tooltip": "このノートは公開されています",
|
||||
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}"
|
||||
"shared-indicator-tooltip-with-url": "このノートは以下で公開されています: {{- url}}",
|
||||
"subtree-hidden-tooltip_other": "{{count}} 個の子ノートがツリーで非表示になっています",
|
||||
"subtree-hidden-moved-title": "{{title}} に追加されました",
|
||||
"subtree-hidden-moved-description-collection": "このコレクションはツリー内の子ノートを非表示にします。",
|
||||
"subtree-hidden-moved-description-other": "このノートのツリーでは子ノートは非表示になっています。"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "一括操作",
|
||||
|
||||
@@ -18,5 +18,65 @@
|
||||
"zpetne_odkazy": {
|
||||
"backlink_one": "{{count}} Tilbakelenke",
|
||||
"backlink_other": "{{count}} Tilbakelenker"
|
||||
},
|
||||
"add_link": {
|
||||
"note": "Notat"
|
||||
},
|
||||
"branch_prefix": {
|
||||
"prefix": "Prefiks : ",
|
||||
"save": "Lagre"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"labels": "Etiketter",
|
||||
"relations": "Relasjoner",
|
||||
"notes": "Notater",
|
||||
"other": "Andre"
|
||||
},
|
||||
"confirm": {
|
||||
"confirmation": "Bekreftelse",
|
||||
"cancel": "Avbryt",
|
||||
"ok": "OK"
|
||||
},
|
||||
"delete_notes": {
|
||||
"close": "Lukk",
|
||||
"cancel": "Avbryt",
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"close": "Lukk",
|
||||
"export": "Eksporter"
|
||||
},
|
||||
"note_type_chooser": {
|
||||
"templates": "Maler"
|
||||
},
|
||||
"help": {
|
||||
"title": "Hurtigveiledning",
|
||||
"troubleshooting": "Feilsøking",
|
||||
"other": "Andre"
|
||||
},
|
||||
"import": {
|
||||
"options": "Alternativer",
|
||||
"import": "Importer"
|
||||
},
|
||||
"include_note": {
|
||||
"label_note": "Notat"
|
||||
},
|
||||
"prompt": {
|
||||
"title": "Ledetekst",
|
||||
"ok": "OK",
|
||||
"defaultTitle": "Ledetekst"
|
||||
},
|
||||
"info": {
|
||||
"closeButton": "Lukk",
|
||||
"okButton": "OK"
|
||||
},
|
||||
"markdown_import": {
|
||||
"import_button": "Importer"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Lukk"
|
||||
},
|
||||
"recent_changes": {
|
||||
"undelete_link": "gjenopprett"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
"title": "Kritische Error",
|
||||
"message": "Een kritieke fout heeft plaatsgevonden waardoor de cliënt zich aanmeldt vanaf het begin:\n\n84X\n\nDit is waarschijnlijk veroorzaakt door een script dat op een onverwachte manier faalt. Probeer de sollicitatie in veilige modus te starten en de kwestie aan te spreken."
|
||||
"message": "Een kritieke fout heeft plaatsgevonden waardoor de applicatie niet kon opstarten:\n\n{{message}}\n\nDit is waarschijnlijk veroorzaakt door een onverwachte fout in een script. Probeer de applicatie op te starten in veilige modus en het probleem op te lossen."
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Starten widget mislukt",
|
||||
@@ -22,7 +22,16 @@
|
||||
"bundle-error": {
|
||||
"title": "Custom script laden mislukt",
|
||||
"message": "Script van notitie met ID \"{{id}}\", getiteld \"{{title}}\" kon niet worden uitgevoerd vanwege:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"scripting-error": "Error met script: {{title}}",
|
||||
"widget-list-error": {
|
||||
"title": "Kon geen lijst met widgets ophalen van de server"
|
||||
},
|
||||
"widget-render-error": {
|
||||
"title": "React-widget kon niet geladen worden"
|
||||
},
|
||||
"widget-missing-parent": "Widget heeft niet het verplichte '{{property}}'-veld gedefinieerd.\n\nAls dit script is bedoeld om zonder interface te draaien, gebruik dan in plaats daarvan '#run=frontendStartup'.",
|
||||
"open-script-note": "Open scriptnotitie"
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Voeg link toe",
|
||||
@@ -41,7 +50,8 @@
|
||||
"help_on_tree_prefix": "Help bij boomvoorvoegsel",
|
||||
"prefix": "Voorvoegsel: ",
|
||||
"edit_branch_prefix_multiple": "Bewerk zijtakvoorvoegsel voor {{count}} zijtakken",
|
||||
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen."
|
||||
"branch_prefix_saved_multiple": "Vertakkingsvoorvoegsel opgeslagen voor {{count}} vertakkingen.",
|
||||
"affected_branches": "Aangetaste takken ({{count}}):"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Bulk acties",
|
||||
@@ -54,7 +64,8 @@
|
||||
"labels": "Labels",
|
||||
"relations": "Relaties",
|
||||
"notes": "Notities",
|
||||
"other": "Andere"
|
||||
"other": "Andere",
|
||||
"include_descendants": "Tel afstammelingen van de geselecteerde notities mee"
|
||||
},
|
||||
"calendar": {
|
||||
"april": "April",
|
||||
@@ -78,5 +89,35 @@
|
||||
},
|
||||
"show_toc_widget_button": {
|
||||
"show_toc": "Laat Inhoudsopgave zien"
|
||||
},
|
||||
"status_bar": {
|
||||
"note_paths_one": "{{count}} pad",
|
||||
"note_paths_other": "{{count}} paden",
|
||||
"note_paths_title": "Notitiepaden",
|
||||
"code_note_switcher": "Verander de taalmodus"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Notitie-attributen"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Geen informatie voor deze notitie",
|
||||
"empty_button": "Verberg dit paneel",
|
||||
"toggle": "Schakel rechterpaneel in/uit",
|
||||
"custom_widget_go_to_source": "Go naar de broncode"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} bijlage",
|
||||
"attachments_other": "{{count}} bijlagen",
|
||||
"layers_one": "{{count}} laag",
|
||||
"layers_other": "{{count}} lagen",
|
||||
"pages_one": "{{count}} pagina",
|
||||
"pages_other": "{{count}} pagina's",
|
||||
"pages_alt": "Pagina {{pageNumber}}",
|
||||
"pages_loading": "Laden..."
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Kloon de notities naar...",
|
||||
"help_on_links": "Hulp op links",
|
||||
"notes_to_clone": "Notities om te klonen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,16 @@
|
||||
"bundle-error": {
|
||||
"title": "Falha para carregar o script customizado",
|
||||
"message": "O script da nota com ID \"{{id}}\", intitulada \"{{title}}\", não pôde ser executado devido a:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"widget-list-error": {
|
||||
"title": "Falha ao obter a lista de widgets do servidor"
|
||||
},
|
||||
"scripting-error": "Erro do script específicado: {{title}}",
|
||||
"open-script-note": "Abrir script da nota",
|
||||
"widget-render-error": {
|
||||
"title": "Falha do renderizar um widget React personalizado"
|
||||
},
|
||||
"widget-missing-parent": "Widget adaptado não tem a propriedade '{{property}}' mandatória definida.\n\nSe este script é para ser executado sem um element de UI, usar '#run=frontendStartup'."
|
||||
},
|
||||
"add_link": {
|
||||
"add_link": "Adicionar ligação",
|
||||
@@ -39,7 +48,10 @@
|
||||
"help_on_tree_prefix": "Ajuda sobre o prefixo da árvore de notas",
|
||||
"prefix": "Prefixo: ",
|
||||
"save": "Gravar",
|
||||
"branch_prefix_saved": "O prefixo de ramificação foi gravado."
|
||||
"branch_prefix_saved": "O prefixo de ramificação foi gravado.",
|
||||
"edit_branch_prefix_multiple": "Editar prefixo para {{count}} branches",
|
||||
"branch_prefix_saved_multiple": "Prefixo dos branches foi editado para {{count}} branches.",
|
||||
"affected_branches": "Alterados ({{count}}) branches:"
|
||||
},
|
||||
"bulk_actions": {
|
||||
"bulk_actions": "Ações em massa",
|
||||
@@ -104,7 +116,8 @@
|
||||
"export_status": "Estado da exportação",
|
||||
"export_in_progress": "Exportação em andamento: {{progressCount}}",
|
||||
"export_finished_successfully": "Exportação concluída com sucesso.",
|
||||
"format_pdf": "PDF – para impressão ou compartilhamento."
|
||||
"format_pdf": "PDF – para impressão ou compartilhamento.",
|
||||
"share-format": "HTML para publicação web - usa o mesmo tema que é usado para notas partilhadas, mas pode ser publicado como um site estatico."
|
||||
},
|
||||
"help": {
|
||||
"title": "Folha de Dicas",
|
||||
@@ -158,7 +171,8 @@
|
||||
"showSQLConsole": "mostrar console SQL",
|
||||
"other": "Outros",
|
||||
"quickSearch": "focar no campo de pesquisa rápida",
|
||||
"inPageSearch": "pesquisa na página"
|
||||
"inPageSearch": "pesquisa na página",
|
||||
"editShortcuts": "Editar atalhos do teclado"
|
||||
},
|
||||
"import": {
|
||||
"importIntoNote": "Importar para a nota",
|
||||
@@ -184,7 +198,8 @@
|
||||
},
|
||||
"import-status": "Estado da importação",
|
||||
"in-progress": "Importação em andamento: {{progress}}",
|
||||
"successful": "Importação concluída com sucesso."
|
||||
"successful": "Importação concluída com sucesso.",
|
||||
"importZipRecommendation": "Quando a importar ficheiro ZIP, a hierarquia de notas vai reflectir a estrutura da sub directoria dentro do ficheiro."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Incluir nota",
|
||||
@@ -199,7 +214,8 @@
|
||||
"info": {
|
||||
"modalTitle": "Mensagem informativa",
|
||||
"closeButton": "Fechar",
|
||||
"okButton": "OK"
|
||||
"okButton": "OK",
|
||||
"copy_to_clipboard": "Copiar para a área de transferência"
|
||||
},
|
||||
"jump_to_note": {
|
||||
"search_placeholder": "Pesquise uma nota pelo nome ou digite > para comandos...",
|
||||
@@ -274,7 +290,12 @@
|
||||
"download_button": "Descarregar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamanho do ficheiro:",
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota."
|
||||
"preview_not_available": "A visualização não está disponível para este tipo de nota.",
|
||||
"diff_on": "Mostrar diferenças",
|
||||
"diff_off": "Mostrar conteúdos",
|
||||
"diff_on_hint": "Carregar para mostrar diferenças da fonte da nota",
|
||||
"diff_off_hint": "Carregar para mostrar conteúdos da nota",
|
||||
"diff_not_available": "Diferenças não disponível."
|
||||
},
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Ordenar notas filhas por...",
|
||||
@@ -585,7 +606,18 @@
|
||||
"september": "Setembro",
|
||||
"october": "Outubro",
|
||||
"november": "Novembro",
|
||||
"december": "Dezembro"
|
||||
"december": "Dezembro",
|
||||
"week": "Semana",
|
||||
"week_previous": "Semana anterior",
|
||||
"week_next": "Próxima semana",
|
||||
"month": "Mês",
|
||||
"month_previous": "Mês anterior",
|
||||
"month_next": "Próximo mês",
|
||||
"year": "Ano",
|
||||
"year_previous": "Ano anterior",
|
||||
"year_next": "Próximo ano",
|
||||
"list": "Lista",
|
||||
"today": "Hoje"
|
||||
},
|
||||
"close_pane_button": {
|
||||
"close_this_pane": "Fechar este painel"
|
||||
@@ -628,7 +660,9 @@
|
||||
"about": "Sobre o Trilium Notes",
|
||||
"logout": "Sair",
|
||||
"show-cheatsheet": "Exibir Cheatsheet",
|
||||
"toggle-zen-mode": "Modo Zen"
|
||||
"toggle-zen-mode": "Modo Zen",
|
||||
"new-version-available": "Nova actualização disponível",
|
||||
"download-update": "Obter versão {{latestVersion}}"
|
||||
},
|
||||
"zen_mode": {
|
||||
"button_exit": "Sair do Modo Zen"
|
||||
@@ -666,7 +700,14 @@
|
||||
"convert_into_attachment_failed": "A conversão da nota '{{title}}' falhou.",
|
||||
"convert_into_attachment_successful": "A nota '{{title}}' foi convertida para anexo.",
|
||||
"convert_into_attachment_prompt": "Tem certeza que quer converter a nota '{{title}}' num anexo da nota pai?",
|
||||
"print_pdf": "Exportar como PDF…"
|
||||
"print_pdf": "Exportar como PDF…",
|
||||
"open_note_on_server": "Abrir nota no servidor",
|
||||
"export_as_image": "Exportar como imagem",
|
||||
"note_map": "Mapa de notas",
|
||||
"advanced": "Avançadas",
|
||||
"view_revisions": "Revisões da nota...",
|
||||
"export_as_image_svg": "SVG (vectorial)",
|
||||
"export_as_image_png": "PNG (matricial)"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Componente de botão '{{componentId}}' não possui manipulador de clique definido"
|
||||
@@ -712,19 +753,29 @@
|
||||
"zpetne_odkazy": {
|
||||
"relation": "relação",
|
||||
"backlink_one": "{{count}} Ligação Reversa",
|
||||
"backlink_many": "",
|
||||
"backlink_many": "{{count}} Ligações Reversas",
|
||||
"backlink_other": "{{count}} Ligações Reversas"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Inserir nota filha",
|
||||
"delete_this_note": "Apagar esta nota",
|
||||
"error_cannot_get_branch_id": "Não foi possível obter o branchId para o notePath '{{notePath}} '",
|
||||
"error_unrecognized_command": "Comando não reconhecido {{command}}"
|
||||
"error_unrecognized_command": "Comando não reconhecido {{command}}",
|
||||
"note_revisions": "Revisões da nota"
|
||||
},
|
||||
"note_icon": {
|
||||
"change_note_icon": "Alterar ícone da nota",
|
||||
"search": "Pesquisa:",
|
||||
"reset-default": "Redefinir para o ícone padrão"
|
||||
"reset-default": "Redefinir para o ícone padrão",
|
||||
"filter": "Filtrar",
|
||||
"filter-none": "Todos os icons",
|
||||
"filter-default": "Icons default",
|
||||
"no_results": "Não foram encontrados icons.",
|
||||
"search_placeholder_filtered": "Procurar {{number}} icons no {{name}}",
|
||||
"icon_tooltip": "{{name}}\nPacote de icons: {{iconPack}}",
|
||||
"search_placeholder_one": "Procurar {{number}} icon nos {{count}} pacotes",
|
||||
"search_placeholder_many": "Procurar {{number}} icons em {{count}} pacotes",
|
||||
"search_placeholder_other": "Procurar {{number}} icons nos {{count}} pacotes"
|
||||
},
|
||||
"basic_properties": {
|
||||
"note_type": "Tipo da nota",
|
||||
@@ -745,7 +796,14 @@
|
||||
"calendar": "Calendário",
|
||||
"table": "Tabela",
|
||||
"geo-map": "Mapa geográfico",
|
||||
"board": "Quadro"
|
||||
"board": "Quadro",
|
||||
"expand_first_level": "Expandir descendentes directos",
|
||||
"presentation": "Apresentação",
|
||||
"expand_nth_level": "Expandir {{depth}} níveis",
|
||||
"expand_all_levels": "Expandir todos os níveis",
|
||||
"include_archived_notes": "Mostrar notas arquivadas",
|
||||
"expand_tooltip": "Expande a direcção dos descendentes desta colecção (um nível). Para mais opções, carregar na seta à direita.",
|
||||
"hide_child_notes": "Esconder notas descendentes na árvore"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "Ainda não há nenhuma nota editada neste dia…",
|
||||
@@ -778,7 +836,8 @@
|
||||
},
|
||||
"inherited_attribute_list": {
|
||||
"title": "Atributos Herdados",
|
||||
"no_inherited_attributes": "Nenhum atributo herdado."
|
||||
"no_inherited_attributes": "Nenhum atributo herdado.",
|
||||
"none": "Nenhum"
|
||||
},
|
||||
"note_info_widget": {
|
||||
"note_id": "ID da Nota",
|
||||
@@ -789,7 +848,9 @@
|
||||
"note_size_info": "O tamanho da nota fornece uma estimativa aproximada dos requisitos de armazenamento para esta nota. Leva em conta o conteúdo e o conteúdo das suas revisões de nota.",
|
||||
"calculate": "calcular",
|
||||
"subtree_size": "(tamanho da subárvore: {{size}} em {{count}} notas)",
|
||||
"title": "Informações da nota"
|
||||
"title": "Informações da nota",
|
||||
"mime": "Tipo MIME",
|
||||
"show_similar_notes": "Mostrar notas semelhantes"
|
||||
},
|
||||
"note_map": {
|
||||
"open_full": "Expandir completamente",
|
||||
@@ -852,7 +913,8 @@
|
||||
"search_parameters": "Parâmetros de Pesquisa",
|
||||
"unknown_search_option": "Opção de pesquisa desconhecida {{searchOptionName}}",
|
||||
"search_note_saved": "Nota de pesquisa foi gravada em {{- notePathTitle}}",
|
||||
"actions_executed": "As ações foram executadas."
|
||||
"actions_executed": "As ações foram executadas.",
|
||||
"view_options": "Ver opções:"
|
||||
},
|
||||
"similar_notes": {
|
||||
"title": "Notas Similares",
|
||||
@@ -946,14 +1008,22 @@
|
||||
"no_attachments": "Esta nota não possuí anexos."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores."
|
||||
"no_children_help": "Esta coleção não possui nenhum nota filha, então não há nada para exibir. Veja <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para pormenores.",
|
||||
"drag_locked_title": "Bloqueado para edição",
|
||||
"drag_locked_message": "Arrastar não permitida pois a coleção está bloqueada para edição."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Digite o conteúdo da sua nota de código aqui…"
|
||||
},
|
||||
"editable_text": {
|
||||
"placeholder": "Digite o conteúdo da sua nota aqui…",
|
||||
"auto-detect-language": "Detetado automaticamente"
|
||||
"auto-detect-language": "Detetado automaticamente",
|
||||
"editor_crashed_title": "O editor de texto quebrou",
|
||||
"editor_crashed_details_button": "Ver mais detalhes...",
|
||||
"editor_crashed_details_title": "Informação técnica",
|
||||
"editor_crashed_details_intro": "Se teve este erro várias vezes, considerer reportar no GitHub disponibilizando a informação abaixo.",
|
||||
"editor_crashed_content": "O seu conteudo foi recuperado com sucesso, mas alguns das alterações mais recentes podem não ter sido gravadas.",
|
||||
"keeps-crashing": "Componente de edição a rebentar continuamente. Por favor tentar reiniciar Trilium. Se o problema persistir, considere abrir um bug report."
|
||||
},
|
||||
"empty": {
|
||||
"open_note_instruction": "Abra uma nota a digitar o título da nota no campo abaixo ou escolha uma nota na árvore.",
|
||||
@@ -1081,7 +1151,8 @@
|
||||
"title": "Largura do Conteúdo",
|
||||
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em ecrãs largos.",
|
||||
"max_width_label": "Largura máxima do conteúdo",
|
||||
"max_width_unit": "pixels"
|
||||
"max_width_unit": "pixels",
|
||||
"centerContent": "Manter conteúdo centrado"
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Barra de Título Nativa (requer recarregar a app)",
|
||||
@@ -1113,7 +1184,9 @@
|
||||
"title": "Desempenho",
|
||||
"enable-motion": "Ativar transições e animações",
|
||||
"enable-shadows": "Ativar sombras",
|
||||
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis"
|
||||
"enable-backdrop-effects": "Ativar efeitos de fundo para menus, popups e painéis",
|
||||
"enable-smooth-scroll": "Activar deslocamento suave",
|
||||
"app-restart-required": "(é necessário reiniciar a aplicação para aplicar as alterações)"
|
||||
},
|
||||
"ai_llm": {
|
||||
"not_started": "Não iniciado",
|
||||
@@ -1272,7 +1345,10 @@
|
||||
"title": "Editor"
|
||||
},
|
||||
"code_mime_types": {
|
||||
"title": "Tipos MIME disponíveis no dropdown"
|
||||
"title": "Tipos MIME disponíveis no dropdown",
|
||||
"tooltip_syntax_highlighting": "Destaque de sintaxe",
|
||||
"tooltip_code_block_syntax": "Blocos de código nas notas de texto",
|
||||
"tooltip_code_note_syntax": "Notas de código"
|
||||
},
|
||||
"vim_key_bindings": {
|
||||
"use_vim_keybindings_in_code_notes": "Atribuições de teclas do Vim",
|
||||
@@ -1392,7 +1468,13 @@
|
||||
"min-days-in-first-week": "Mínimo de dias da primeira semana",
|
||||
"first-week-info": "Primeira semana que contenha a primeira Quinta-feira do ano é baseado na <a href=\"https://en.wikipedia.org/wiki/ISO_week_date#First_week\">ISO 8601</a>.",
|
||||
"first-week-warning": "Alterar as opções de primeira semana pode causar duplicidade nas Notas Semanais existentes e estas Notas não serão atualizadas de acordo.",
|
||||
"formatting-locale": "Formato de data e número"
|
||||
"formatting-locale": "Formato de data e número",
|
||||
"tuesday": "Terça-feira",
|
||||
"wednesday": "Quarta-feira",
|
||||
"thursday": "Quinta-feira",
|
||||
"friday": "Sexta-feira",
|
||||
"saturday": "Sábado",
|
||||
"formatting-locale-auto": "Baseado na linguagem da aplicação"
|
||||
},
|
||||
"backup": {
|
||||
"automatic_backup": "Backup automático",
|
||||
@@ -1485,7 +1567,8 @@
|
||||
"oauth_description_warning": "Para ativar o OAuth/OpenID, precisa definir a URL base do OAuth/OpenID, o client ID e o client secret no ficheiro config.ini e reiniciar a aplicação. Se quiser configurar via variáveis de ambiente, defina TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID e TRILIUM_OAUTH_CLIENT_SECRET.",
|
||||
"oauth_user_account": "Conta do Utilizador: ",
|
||||
"oauth_user_email": "E-mail do Utilizador: ",
|
||||
"oauth_user_not_logged_in": "Não está logado!"
|
||||
"oauth_user_not_logged_in": "Não está logado!",
|
||||
"oauth_missing_vars": "Configurações em falta: {{-variables}}"
|
||||
},
|
||||
"shortcuts": {
|
||||
"keyboard_shortcuts": "Atalhos de Teclado",
|
||||
@@ -1585,7 +1668,12 @@
|
||||
"apply-bulk-actions": "Aplicar ações em massa",
|
||||
"converted-to-attachments": "{{count}} notas foram convertidas em anexos.",
|
||||
"convert-to-attachment-confirm": "Tem certeza que deseja converter as notas selecionadas em anexos das suas notas-pai?",
|
||||
"open-in-popup": "Edição rápida"
|
||||
"open-in-popup": "Edição rápida",
|
||||
"open-in-a-new-window": "Abrir numa nova janela",
|
||||
"archive": "Arquivar",
|
||||
"unarchive": "Retirar do arquivo",
|
||||
"hide-subtree": "Esconder sub-árvore",
|
||||
"show-subtree": "Mostrar sub-árvore"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Esta nota é partilhada publicamente em {{- link}}.",
|
||||
@@ -1646,7 +1734,13 @@
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Lista de Destaques",
|
||||
"options": "Opções"
|
||||
"options": "Opções",
|
||||
"no_highlights": "Sem destaques encontrados.",
|
||||
"menu_configure": "Configurar lista de destaques...",
|
||||
"modal_title": "Configurar list de destaques",
|
||||
"title_with_count_one": "{{count}} destaque",
|
||||
"title_with_count_many": "{{count}} destaques",
|
||||
"title_with_count_other": "{{count}} destaques"
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Pesquisa rápida",
|
||||
@@ -1669,16 +1763,43 @@
|
||||
"refresh-saved-search-results": "Atualizar resultados de pesquisa gravados",
|
||||
"create-child-note": "Criar nota filha",
|
||||
"unhoist": "Desafixar",
|
||||
"toggle-sidebar": "Alternar barra lateral"
|
||||
"toggle-sidebar": "Alternar barra lateral",
|
||||
"dropping-not-allowed": "Largar notas nesta localização não é permitida",
|
||||
"clone-indicator-tooltip": "Esta nota tem {{- count}} ascendentes: {{- parents}}",
|
||||
"shared-indicator-tooltip": "Esta nota está partilhada publicamente",
|
||||
"shared-indicator-tooltip-with-url": "Esta nota está partilhada publicamente em: {{- url}}",
|
||||
"subtree-hidden-moved-title": "Adicionar ao {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "Esta colecção esconde as notas descendentes na árvore.",
|
||||
"subtree-hidden-moved-description-other": "Notas descendentes estão escondidades na árvore para esta nota.",
|
||||
"subtree-hidden-tooltip_one": "{{count}} nota descendentes escondidas da árvore",
|
||||
"subtree-hidden-tooltip_many": "{{count}} notas descendentes escondidas da árvore",
|
||||
"subtree-hidden-tooltip_other": "{{count}} notas descendentes escondidas da árvore",
|
||||
"clone-indicator-tooltip-single": "Esta nota está clonada (1 additional parent: {{- parent}})"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Manter Janela no Topo"
|
||||
},
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'"
|
||||
"could_not_find_typewidget": "Não foi possível encontrar typeWidget para o tipo '{{type}}'",
|
||||
"print_report_collection_details_button": "Ver detalhes",
|
||||
"printing": "Impressão em progresso...",
|
||||
"printing_pdf": "Exportação PDF em progresso...",
|
||||
"print_report_title": "Imprimir relatório",
|
||||
"print_report_collection_details_ignored_notes": "Ignorar notas",
|
||||
"print_report_collection_content_one": "{{count}} nota na colecção não pode ser impressa porque não é suportado ou está protegida.",
|
||||
"print_report_collection_content_many": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas.",
|
||||
"print_report_collection_content_other": "{{count}} notas na colecção não podem ser impressas porque não é suportado ou estão protegidas."
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "digite o título da nota aqui..."
|
||||
"placeholder": "digite o título da nota aqui...",
|
||||
"promoted_attributes": "Atributos destacados",
|
||||
"created_on": "Criado em <Value />",
|
||||
"last_modified": "Modificado em <Value />",
|
||||
"note_type_switcher_label": "Alterar de {{type}} para:",
|
||||
"note_type_switcher_others": "Outro tipo de nota",
|
||||
"note_type_switcher_templates": "Template",
|
||||
"note_type_switcher_collection": "Colecção",
|
||||
"edited_notes": "Notas editadas neste dia"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Nenhuma nota encontrada para os parâmetros de pesquisa digitados.",
|
||||
@@ -1707,7 +1828,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Tabela de Conteúdos",
|
||||
"options": "Opções"
|
||||
"options": "Opções",
|
||||
"no_headings": "Sem cabeçalhos."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "O ficheiro <code class=\"file-path\"></code> foi modificado pela última vez em <span class=\"file-last-modified\"></span>.",
|
||||
@@ -1750,7 +1872,9 @@
|
||||
"ws": {
|
||||
"sync-check-failed": "A verificação de sincronização falhou!",
|
||||
"consistency-checks-failed": "A verificação de consistência falhou! Veja os logs para pormenores.",
|
||||
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console."
|
||||
"encountered-error": "Encontrado o erro \"{{message}}\", verifique o console.",
|
||||
"lost-websocket-connection-title": "Perdida conexão com o servidor",
|
||||
"lost-websocket-connection-message": "Verifique a configuração da proxy inversa (e.g. nginx ou Apache) para assegurar conexões WebSocket estão permitidas e não bloqueadas."
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "A nota solicitada '{{requestedNote}}' está fora da árvore da nota fixada '{{hoistedNote}}' e precisa desafixar para aceder a nota. Quer prosseguir e desafixar?"
|
||||
@@ -1806,7 +1930,8 @@
|
||||
"copy-link": "Copiar ligação",
|
||||
"paste": "Colar",
|
||||
"paste-as-plain-text": "Colar como texto sem formatação",
|
||||
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}"
|
||||
"search_online": "Pesquisar por \"{{term}}\" com {{searchEngine}}",
|
||||
"search_in_trilium": "A procurar \"{{term}}\" no Trilium"
|
||||
},
|
||||
"image_context_menu": {
|
||||
"copy_reference_to_clipboard": "Copiar referência para a área de transferência",
|
||||
@@ -1816,7 +1941,8 @@
|
||||
"open_note_in_new_tab": "Abrir nota em nova guia",
|
||||
"open_note_in_new_split": "Abrir nota em nova divisão",
|
||||
"open_note_in_new_window": "Abrir nota em nova janela",
|
||||
"open_note_in_popup": "Edição rápida"
|
||||
"open_note_in_popup": "Edição rápida",
|
||||
"open_note_in_other_split": "Abrir nota noutro separador"
|
||||
},
|
||||
"electron_integration": {
|
||||
"desktop-application": "Aplicação Desktop",
|
||||
@@ -1824,7 +1950,8 @@
|
||||
"native-title-bar-description": "Para Windows e macOS, manter a barra de título nativa desativada faz a aplicação parecer mais compacta. No Linux, manter a barra de título nativa ativada faz a aplicação se integrar melhor com o restante do sistema.",
|
||||
"background-effects": "Ativar efeitos de fundo (apenas Windows 11)",
|
||||
"restart-app-button": "Reiniciar a aplicação para ver as alterações",
|
||||
"zoom-factor": "Fator de Zoom"
|
||||
"zoom-factor": "Fator de Zoom",
|
||||
"background-effects-description": "O Mica adiciona um desfoque, fundo estiloso as janelas da aplicação, criando uma profundidade e aspecto moderno. \"Barra de titulo nativa\" deve estar inactiva."
|
||||
},
|
||||
"note_autocomplete": {
|
||||
"search-for": "Pesquisar por \"{{term}}\"",
|
||||
@@ -1884,7 +2011,8 @@
|
||||
},
|
||||
"note_language": {
|
||||
"not_set": "Não atribuído",
|
||||
"configure-languages": "Configurar idiomas..."
|
||||
"configure-languages": "Configurar idiomas...",
|
||||
"help-on-languages": "Ajuda nas linguagens de conteúdos..."
|
||||
},
|
||||
"content_language": {
|
||||
"title": "Idiomas do conteúdo",
|
||||
@@ -1902,7 +2030,8 @@
|
||||
"button_title": "Exportar diagrama como PNG"
|
||||
},
|
||||
"svg": {
|
||||
"export_to_png": "O diagrama não pôde ser exportado como PNG."
|
||||
"export_to_png": "O diagrama não pôde ser exportado como PNG.",
|
||||
"export_to_svg": "O diagrama não pode ser exportado para SVG."
|
||||
},
|
||||
"code_theme": {
|
||||
"title": "Aparência",
|
||||
@@ -1921,7 +2050,11 @@
|
||||
"editorfeatures": {
|
||||
"title": "Recursos",
|
||||
"emoji_completion_enabled": "Ativar auto-completar de Emoji",
|
||||
"note_completion_enabled": "Ativar auto-completar de notas"
|
||||
"note_completion_enabled": "Ativar auto-completar de notas",
|
||||
"emoji_completion_description": "Se activo, emojis podem ser facilmente inseridos em texto ao pressionar `:`, seguido do nome de um emoji.",
|
||||
"note_completion_description": "Se activo, links para notas podem ser criadas ao escrever `@` seguido do titulo de uma nota.",
|
||||
"slash_commands_enabled": "Activar comentários simples",
|
||||
"slash_commands_description": "Se activo, editar comandos como inserir quebras de linha ou cabeçalhos podem ser activado/inactivado ao escrever `/`."
|
||||
},
|
||||
"table_view": {
|
||||
"new-row": "Nova linha",
|
||||
@@ -1963,7 +2096,16 @@
|
||||
"delete-column": "Apagar coluna",
|
||||
"delete-column-confirmation": "Tem certeza que deseja apagar esta coluna? O atributo correspondente também será apagado de todas as notas abaixo desta coluna.",
|
||||
"new-item": "Novo elemento",
|
||||
"add-column": "Adicionar Coluna"
|
||||
"add-column": "Adicionar Coluna",
|
||||
"delete-note": "Apagar nota...",
|
||||
"remove-from-board": "Remover do quadro",
|
||||
"archive-note": "Arquivar nota",
|
||||
"new-item-placeholder": "Inserir titulo da nota...",
|
||||
"add-column-placeholder": "Inserir nome da coluna...",
|
||||
"edit-note-title": "Clicar para editar o titulo da nota",
|
||||
"unarchive-note": "Remover nota do arquivo",
|
||||
"edit-column-title": "Click para editar titulo da coluna",
|
||||
"column-already-exists": "Esta coluna já existe no quadro."
|
||||
},
|
||||
"command_palette": {
|
||||
"tree-action-name": "Árvore: {{name}}",
|
||||
@@ -1994,16 +2136,146 @@
|
||||
"background_effects_title": "Efeitos de fundo estão estáveis agora",
|
||||
"background_effects_message": "Em dispositivos Windows, efeitos de fundo estão estáveis agora. Os efeitos de fundo adicionam um toque de cor à interface do utilizador borrando o plano de fundo atrás dela. Esta técnica também é usada noutras aplicações como o Windows Explorer.",
|
||||
"background_effects_button": "Ativar os efeitos de fundo",
|
||||
"dismiss": "Dispensar"
|
||||
"dismiss": "Dispensar",
|
||||
"new_layout_title": "Novo titulo do layout",
|
||||
"new_layout_button": "Mais informação",
|
||||
"new_layout_message": "Estamos a introduzir um layout modernizado para o Trilium. A faixa foi removida e está integrada na interface principal, com uma nota barra de estado e secções expansíveis (como as propriedades próprias) a tomar papéis principais.\n\nO novo layout está activo por defeito, e pode ser temporáriamente disabilidade em Opções → Aparência."
|
||||
},
|
||||
"settings": {
|
||||
"related_settings": "Configurações relacionadas"
|
||||
},
|
||||
"settings_appearance": {
|
||||
"related_code_blocks": "Esquema de cores para blocos de código em notas de texto",
|
||||
"related_code_notes": "Esquema de cores para notas de código"
|
||||
"related_code_notes": "Esquema de cores para notas de código",
|
||||
"ui": "Interface do utilizador",
|
||||
"ui_old_layout": "Layout antigo",
|
||||
"ui_new_layout": "Nova aparência"
|
||||
},
|
||||
"units": {
|
||||
"percentage": "%"
|
||||
},
|
||||
"experimental_features": {
|
||||
"title": "Opções experimentais",
|
||||
"new_layout_name": "Novo layout",
|
||||
"new_layout_description": "Experimente o novo layout para um aspecto moderno e melhor estabilidade. Sujeito a grandes alterações nas próximas publicações.",
|
||||
"disclaimer": "Estas opções são experimentais e podem causar instabilidade. Usar com cuidado."
|
||||
},
|
||||
"read-only-info": {
|
||||
"read-only-note": "Actualmente a ver em modo de leitura.",
|
||||
"edit-note": "Editar nota",
|
||||
"auto-read-only-note": "Esta nota está a ser mostrada em modo de leitura para um carregamento mais rápido."
|
||||
},
|
||||
"presentation_view": {
|
||||
"edit-slide": "Editar este slide",
|
||||
"start-presentation": "Iniciar apresentação",
|
||||
"slide-overview": "Alternar visão geral dos slides"
|
||||
},
|
||||
"calendar_view": {
|
||||
"delete_note": "Apagar nota..."
|
||||
},
|
||||
"pagination": {
|
||||
"page_title": "Página {{startIndex}} - {{endIndex}}",
|
||||
"total_notes": "{{count}} notas"
|
||||
},
|
||||
"collections": {
|
||||
"rendering_error": "Sem possíbilidade de mostrar conteúdos devido a um erro."
|
||||
},
|
||||
"note-color": {
|
||||
"clear-color": "Remover cor da nota",
|
||||
"set-color": "Atribuir cor da nota",
|
||||
"set-custom-color": "Afectar cor personalizada da nota"
|
||||
},
|
||||
"popup-editor": {
|
||||
"maximize": "Alterar para editor completo"
|
||||
},
|
||||
"server": {
|
||||
"unknown_http_error_title": "Erro na comunicação com servidor",
|
||||
"unknown_http_error_content": "Código de estado: {{statusCode}}\nURL: {{method}} {{url}}\nMessagem: {{message}}",
|
||||
"traefik_blocks_requests": "Se está a usar o Traefik, este introduz uma alteração que afecta a comunicação com o servidor."
|
||||
},
|
||||
"tab_history_navigation_buttons": {
|
||||
"go-back": "Ir para a nota anterior",
|
||||
"go-forward": "Ir para nota seguinte"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"hoisted_badge": "Içado",
|
||||
"workspace_badge": "Área de trabalho",
|
||||
"scroll_to_top_title": "Saltar para o início da nota",
|
||||
"create_new_note": "Criar nova nota descendente",
|
||||
"empty_hide_archived_notes": "Esconder notas arquivadas",
|
||||
"hoisted_badge_title": "Retirar de içado"
|
||||
},
|
||||
"breadcrumb_badges": {
|
||||
"read_only_explicit": "Modo de leitura",
|
||||
"read_only_auto": "Modo de leitura automático",
|
||||
"read_only_temporarily_disabled": "Editável temporáriamente",
|
||||
"read_only_auto_description": "Esta nota foi automaticamente colocada em modo de leitura por razões de performance. Este limite automatico é ajustável nas configurações.\n\nClicar para editar temporáriamente.",
|
||||
"read_only_temporarily_disabled_description": "Esta nota está editável, mas normalmente está em modo de leitura. A nova vai regressar para mode de leitura assim que navegar para outra nota.\n\nClicar para reactivar o modo de leitura.",
|
||||
"read_only_explicit_description": "Esta nota foi manualmente colocada em modo de leitura.\nClicar para editar temporáriamente.",
|
||||
"shared_publicly": "Partilhado publicamente",
|
||||
"shared_locally": "Partilhado localmente",
|
||||
"shared_copy_to_clipboard": "Copiar link para a área de transferência",
|
||||
"shared_open_in_browser": "Abrir link no browser",
|
||||
"shared_unshare": "Remover partilha",
|
||||
"clipped_note_description": "Esta nota foi retirar do {{url}}.\n\nClicar para navegar no código fonte da página.",
|
||||
"clipped_note": "Web clipe",
|
||||
"execute_script": "Correr script",
|
||||
"execute_script_description": "Esta nota é uma nota de script. Clicar para executar o script.",
|
||||
"execute_sql": "Correr SQL",
|
||||
"execute_sql_description": "Esta nota é uma nota de SQL. Clicar para executar script SQL.",
|
||||
"save_status_saved": "Guardar",
|
||||
"save_status_saving": "A guardar...",
|
||||
"save_status_unsaved": "Não gravado",
|
||||
"save_status_error": "Gravar falhou",
|
||||
"save_status_saving_tooltip": "Alterações estão a ser guardadas",
|
||||
"save_status_unsaved_tooltip": "Existem alterações não guardadas. Serão guardadas automaticamente em breve.",
|
||||
"save_status_error_tooltip": "Ocorreu um erro ao guardar a nota. Se possível, tente copiar os conteúdos da nota para outro local e reiniciar a aplicação."
|
||||
},
|
||||
"status_bar": {
|
||||
"language_title": "Alterar lingua do conteúdo",
|
||||
"note_info_title": "Ver informação da nota (e.g., datas, tamanho da nota)",
|
||||
"backlinks_one": "{{count}} backlink",
|
||||
"backlinks_many": "{{count}} backlinks",
|
||||
"backlinks_other": "{{count}} backlinks",
|
||||
"backlinks_title_one": "Ver backlink",
|
||||
"backlinks_title_many": "Ver backlinks",
|
||||
"backlinks_title_other": "Ver backlinks",
|
||||
"attachments_one": "{{count}} anexo",
|
||||
"attachments_many": "{{count}} anexos",
|
||||
"attachments_other": "{{count}} anexos",
|
||||
"attachments_title_one": "Ver anexo num novo separador",
|
||||
"attachments_title_many": "Ver anexos num novo separador",
|
||||
"attachments_title_other": "Ver anexos num novo separador",
|
||||
"attributes_one": "{{count}} atributo",
|
||||
"attributes_many": "{{count}} atributos",
|
||||
"attributes_other": "{{count}} atributos",
|
||||
"attributes_title": "Atributos próprios e herdados",
|
||||
"note_paths_one": "{{count}} caminho",
|
||||
"note_paths_many": "{{count}} caminhos",
|
||||
"note_paths_other": "{{count}} caminhos",
|
||||
"note_paths_title": "Caminhos da nota",
|
||||
"code_note_switcher": "Alterar modo de linguagem"
|
||||
},
|
||||
"attributes_panel": {
|
||||
"title": "Atributos da nota"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Nada para mostrar nesta nota",
|
||||
"empty_button": "Esconder painél",
|
||||
"toggle": "Alterar painel direito",
|
||||
"custom_widget_go_to_source": "Ir para código fonte"
|
||||
},
|
||||
"pdf": {
|
||||
"attachments_one": "{{count}} anexo pdf",
|
||||
"attachments_many": "{{count}} anexos pdf",
|
||||
"attachments_other": "{{count}} anexos pdf",
|
||||
"layers_one": "{{count}} camada",
|
||||
"layers_many": "{{count}} camadas",
|
||||
"layers_other": "{{count}} camadas",
|
||||
"pages_one": "{{count}} página",
|
||||
"pages_many": "{{count}} páginas",
|
||||
"pages_other": "{{count}} páginas",
|
||||
"pages_alt": "Página {{pageNumber}}",
|
||||
"pages_loading": "A carregar..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +797,8 @@
|
||||
"expand_tooltip": "展開此集合的直接子級(單層深度)。按下右側箭頭以查看更多選項。",
|
||||
"expand_first_level": "展開直接子級",
|
||||
"expand_nth_level": "展開 {{depth}} 層",
|
||||
"expand_all_levels": "展開所有層級"
|
||||
"expand_all_levels": "展開所有層級",
|
||||
"hide_child_notes": "隱藏樹中的子筆記"
|
||||
},
|
||||
"edited_notes": {
|
||||
"no_edited_notes_found": "今天還沒有編輯過的筆記...",
|
||||
@@ -1466,7 +1467,10 @@
|
||||
"duplicate": "複製副本",
|
||||
"open-in-popup": "快速編輯",
|
||||
"archive": "封存",
|
||||
"unarchive": "解除封存"
|
||||
"unarchive": "解除封存",
|
||||
"open-in-a-new-window": "在新視窗打開",
|
||||
"hide-subtree": "隱藏子階層",
|
||||
"show-subtree": "顯示子階層"
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "如需幫助,請訪問 <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>。",
|
||||
@@ -1560,7 +1564,11 @@
|
||||
"clone-indicator-tooltip": "此筆記有 {{- count}} 個父級:{{- parents}}",
|
||||
"clone-indicator-tooltip-single": "此筆記已克隆(新增 1 個父級:{{- parent}})",
|
||||
"shared-indicator-tooltip": "此筆記已公開分享",
|
||||
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}"
|
||||
"shared-indicator-tooltip-with-url": "此筆記已公開分享至:{{- url}}",
|
||||
"subtree-hidden-tooltip_one": "從樹中隱藏的 {{count}} 篇子筆記",
|
||||
"subtree-hidden-moved-title": "已新增至 {{title}}",
|
||||
"subtree-hidden-moved-description-collection": "此集合隱藏其樹中的子筆記。",
|
||||
"subtree-hidden-moved-description-other": "子筆記隱藏於此筆記的樹中。"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "保持此視窗置頂"
|
||||
|
||||
21
apps/client/src/types-fancytree.d.ts
vendored
@@ -69,7 +69,7 @@ declare namespace Fancytree {
|
||||
debug(msg: any): void;
|
||||
|
||||
/** Expand (or collapse) all parent nodes. */
|
||||
expandAll(flag?: boolean, options?: Object): void;
|
||||
expandAll(flag?: boolean, options?: object): void;
|
||||
|
||||
/** [ext-filter] Dimm or hide whole branches.
|
||||
* @returns {integer} count
|
||||
@@ -221,6 +221,7 @@ declare namespace Fancytree {
|
||||
branchId: string;
|
||||
isProtected: boolean;
|
||||
noteType: NoteType;
|
||||
subtreeHidden: boolean;
|
||||
}
|
||||
|
||||
interface FancytreeNewNode extends FancytreeNodeData {
|
||||
@@ -369,7 +370,7 @@ declare namespace Fancytree {
|
||||
* @param mode 'before', 'after', or 'child' (default='child')
|
||||
* @param init NodeData (or simple title string)
|
||||
*/
|
||||
editCreateNode(mode?: string, init?: Object): void;
|
||||
editCreateNode(mode?: string, init?: object): void;
|
||||
|
||||
/** [ext-edit] Stop inline editing.
|
||||
*
|
||||
@@ -526,7 +527,7 @@ declare namespace Fancytree {
|
||||
*
|
||||
* @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
|
||||
*/
|
||||
makeVisible(opts?: Object): JQueryPromise<any>;
|
||||
makeVisible(opts?: object): JQueryPromise<any>;
|
||||
|
||||
/** Move this node to targetNode.
|
||||
*
|
||||
@@ -589,25 +590,25 @@ declare namespace Fancytree {
|
||||
* @param effects animation options.
|
||||
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
|
||||
*/
|
||||
scrollIntoView(effects?: boolean, options?: Object): JQueryPromise<any>;
|
||||
scrollIntoView(effects?: boolean, options?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param effects animation options.
|
||||
* @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane.
|
||||
*/
|
||||
scrollIntoView(effects?: Object, options?: Object): JQueryPromise<any>;
|
||||
scrollIntoView(effects?: object, options?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param flag pass false to deactivate
|
||||
* @param opts additional options. Defaults to {noEvents: false}
|
||||
*/
|
||||
setActive(flag?: boolean, opts?: Object): JQueryPromise<any>;
|
||||
setActive(flag?: boolean, opts?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* @param flag pass false to collapse.
|
||||
* @param opts additional options. Defaults to {noAnimation:false, noEvents:false}
|
||||
*/
|
||||
setExpanded(flag?: boolean, opts?: Object): JQueryPromise<any>;
|
||||
setExpanded(flag?: boolean, opts?: object): JQueryPromise<any>;
|
||||
|
||||
/**
|
||||
* Set keyboard focus to this node.
|
||||
@@ -1109,7 +1110,7 @@ declare namespace Fancytree {
|
||||
/** class names added to the node markup (separate with space) */
|
||||
extraClasses?: string | undefined;
|
||||
/** all properties from will be copied to `node.data` */
|
||||
data?: Object | undefined;
|
||||
data?: object | undefined;
|
||||
|
||||
/** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */
|
||||
iconTooltip?: string | undefined;
|
||||
@@ -1160,7 +1161,7 @@ declare namespace Fancytree {
|
||||
|
||||
escapeHtml(s: string): string;
|
||||
|
||||
getEventTarget(event: Event): Object;
|
||||
getEventTarget(event: Event): object;
|
||||
|
||||
getEventTargetType(event: Event): string;
|
||||
|
||||
@@ -1179,7 +1180,7 @@ declare namespace Fancytree {
|
||||
parseHtml($ul: JQuery): NodeData[];
|
||||
|
||||
/** Add Fancytree extension definition to the list of globally available extensions. */
|
||||
registerExtension(definition: Object): void;
|
||||
registerExtension(definition: object): void;
|
||||
|
||||
unescapeHtml(s: string): string;
|
||||
|
||||
|
||||
14
apps/client/src/types.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { IconRegistry } from "@triliumnext/commons";
|
||||
import { IconRegistry, Locale } from "@triliumnext/commons";
|
||||
|
||||
import appContext, { AppContext } from "./components/app_context";
|
||||
import type FNote from "./entities/fnote";
|
||||
@@ -36,6 +36,7 @@ interface CustomGlobals {
|
||||
isProtectedSessionAvailable: boolean;
|
||||
isDev: boolean;
|
||||
isMainWindow: boolean;
|
||||
windowId: string;
|
||||
maxEntityChangeIdAtLoad: number;
|
||||
maxEntityChangeSyncIdAtLoad: number;
|
||||
assetPath: string;
|
||||
@@ -47,14 +48,25 @@ interface CustomGlobals {
|
||||
platform?: typeof process.platform;
|
||||
linter: typeof lint;
|
||||
hasNativeTitleBar: boolean;
|
||||
hasBackgroundEffects: boolean;
|
||||
isElectron: boolean;
|
||||
isRtl: boolean;
|
||||
iconRegistry: IconRegistry;
|
||||
themeCssUrl: string;
|
||||
themeUseNextAsBase?: "next" | "next-light" | "next-dark";
|
||||
iconPackCss: string;
|
||||
headingStyle: "plain" | "underline" | "markdown";
|
||||
layoutOrientation: "vertical" | "horizontal";
|
||||
currentLocale: Locale;
|
||||
}
|
||||
|
||||
type RequireMethod = (moduleName: string) => any;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
$: JQueryStatic;
|
||||
jQuery: JQueryStatic;
|
||||
|
||||
logError(message: string);
|
||||
logInfo(message: string);
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ export default function NoteDetail() {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={`note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
|
||||
>
|
||||
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
|
||||
return <NoteDetailWrapper
|
||||
|
||||
@@ -44,6 +44,7 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
|
||||
const noteIds = useFilteredNoteIds(note, unfilteredNoteIds);
|
||||
const { pageNotes, ...pagination } = usePagination(note, noteIds);
|
||||
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
|
||||
|
||||
return (
|
||||
<div class="note-list grid-view">
|
||||
@@ -52,7 +53,7 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
|
||||
<div class="note-list-container use-tn-links">
|
||||
{pageNotes?.map(childNote => (
|
||||
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} />
|
||||
<GridNoteCard note={childNote} parentNote={note} highlightedTokens={highlightedTokens} includeArchived={includeArchived} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -94,14 +95,16 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
</h5>
|
||||
|
||||
{isExpanded && <>
|
||||
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList />
|
||||
<NoteContent note={note} highlightedTokens={highlightedTokens} noChildrenList includeArchivedNotes={includeArchived} />
|
||||
<NoteChildren note={note} parentNote={parentNote} highlightedTokens={highlightedTokens} currentLevel={currentLevel} expandDepth={expandDepth} includeArchived={includeArchived} />
|
||||
</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined }) {
|
||||
function GridNoteCard({ note, parentNote, highlightedTokens, includeArchived }: { note: FNote, parentNote: FNote, highlightedTokens: string[] | null | undefined, includeArchived: boolean }) {
|
||||
const titleRef = useRef<HTMLSpanElement>(null);
|
||||
const [ noteTitle, setNoteTitle ] = useState<string>();
|
||||
const notePath = getNotePath(parentNote, note);
|
||||
|
||||
return (
|
||||
@@ -120,6 +123,7 @@ function GridNoteCard({ note, parentNote, highlightedTokens }: { note: FNote, pa
|
||||
note={note}
|
||||
trim
|
||||
highlightedTokens={highlightedTokens}
|
||||
includeArchivedNotes={includeArchived}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -136,14 +140,22 @@ function NoteAttributes({ note }: { note: FNote }) {
|
||||
return <span className="note-list-attributes" ref={ref} />;
|
||||
}
|
||||
|
||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens }: { note: FNote, trim?: boolean, noChildrenList?: boolean, highlightedTokens: string[] | null | undefined }) {
|
||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
||||
note: FNote;
|
||||
trim?: boolean;
|
||||
noChildrenList?: boolean;
|
||||
highlightedTokens: string[] | null | undefined;
|
||||
includeArchivedNotes: boolean;
|
||||
}) {
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||
|
||||
useEffect(() => {
|
||||
content_renderer.getRenderedContent(note, {
|
||||
trim,
|
||||
noChildrenList
|
||||
noChildrenList,
|
||||
noIncludedNotes: true,
|
||||
includeArchivedNotes
|
||||
})
|
||||
.then(({ $renderedContent, type }) => {
|
||||
if (!contentRef.current) return;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useCallback, useLayoutEffect, useState } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import froca from "../../services/froca";
|
||||
import { isDesktop, isMobile } from "../../services/utils";
|
||||
import CalendarWidget from "./CalendarWidget";
|
||||
import SpacerWidget from "./SpacerWidget";
|
||||
import BookmarkButtons from "./BookmarkButtons";
|
||||
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
|
||||
import SyncStatus from "./SyncStatus";
|
||||
import HistoryNavigationButton from "./HistoryNavigation";
|
||||
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||
import BookmarkButtons from "./BookmarkButtons";
|
||||
import CalendarWidget from "./CalendarWidget";
|
||||
import HistoryNavigationButton from "./HistoryNavigation";
|
||||
import { LaunchBarContext } from "./launch_bar_widgets";
|
||||
import { AiChatButton, CommandButton, CustomWidget, NoteLauncher, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
|
||||
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
|
||||
import SpacerWidget from "./SpacerWidget";
|
||||
import SyncStatus from "./SyncStatus";
|
||||
|
||||
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const childNotes = useLauncherChildNotes();
|
||||
@@ -34,18 +35,19 @@ export default function LauncherContainer({ isHorizontalLayout }: { isHorizontal
|
||||
}}>
|
||||
{childNotes?.map(childNote => {
|
||||
if (childNote.type !== "launcher") {
|
||||
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
console.warn(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
|
||||
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />;
|
||||
})}
|
||||
</LaunchBarContext.Provider>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
|
||||
@@ -72,7 +74,7 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
const builtinWidget = note.getLabelValue("builtinWidget");
|
||||
switch (builtinWidget) {
|
||||
case "calendar":
|
||||
return <CalendarWidget launcherNote={note} />
|
||||
return <CalendarWidget launcherNote={note} />;
|
||||
case "spacer":
|
||||
// || has to be inside since 0 is a valid value
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
@@ -86,15 +88,15 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
case "syncStatus":
|
||||
return <SyncStatus />;
|
||||
case "backInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
|
||||
case "forwardInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
|
||||
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />;
|
||||
case "todayInJournal":
|
||||
return <TodayLauncher launcherNote={note} />
|
||||
return <TodayLauncher launcherNote={note} />;
|
||||
case "quickSearch":
|
||||
return <QuickSearchLauncherWidget />
|
||||
return <QuickSearchLauncherWidget />;
|
||||
case "aiChatLauncher":
|
||||
return <AiChatButton launcherNote={note} />
|
||||
return <AiChatButton launcherNote={note} />;
|
||||
default:
|
||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
|
||||
@@ -338,19 +338,19 @@ interface AttributesProps extends StatusBarContext {
|
||||
function AttributesButton({ note, attributesShown, setAttributesShown }: AttributesProps) {
|
||||
const [ count, setCount ] = useState(note.attributes.length);
|
||||
|
||||
const refreshCount = useCallback((note: FNote) => {
|
||||
const getAttributeCount = useCallback((note: FNote) => {
|
||||
return note.getAttributes().filter(a => !a.isAutoLink).length;
|
||||
}, []);
|
||||
|
||||
// React to note changes.
|
||||
useEffect(() => {
|
||||
setCount(refreshCount(note));
|
||||
}, [ note, refreshCount ]);
|
||||
setCount(getAttributeCount(note));
|
||||
}, [ note, getAttributeCount ]);
|
||||
|
||||
// React to changes in count.
|
||||
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
||||
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
||||
setCount(refreshCount(note));
|
||||
setCount(getAttributeCount(note));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -82,6 +82,13 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption
|
||||
))}
|
||||
{properties.length > 0 && <FormDropdownDivider />}
|
||||
|
||||
<ViewProperty note={note} property={{
|
||||
type: "checkbox",
|
||||
icon: "bx bx-hide",
|
||||
label: t("book_properties.hide_child_notes"),
|
||||
bindToLabel: "subtreeHidden"
|
||||
} as CheckBoxProperty} />
|
||||
|
||||
<ViewProperty note={note} property={{
|
||||
type: "checkbox",
|
||||
icon: "bx bx-archive",
|
||||
|
||||
20
apps/client/src/widgets/note_tree.css
Normal file
@@ -0,0 +1,20 @@
|
||||
#left-pane .tree-wrapper {
|
||||
.note-indicator-icon.subtree-hidden-badge {
|
||||
font-family: inherit !important;
|
||||
margin-inline: 0.5em;
|
||||
margin-top: 0.3em;
|
||||
background: var(--left-pane-item-action-button-background);
|
||||
color: var(--left-pane-item-action-button-color);
|
||||
padding: 0.1em 0.6em;
|
||||
border-radius: 0.5em;
|
||||
font-size: 0.7rem;
|
||||
font-weight: normal;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.spotlighted-node {
|
||||
opacity: 0.8;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
|
||||
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
|
||||
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
|
||||
import "../stylesheets/tree.css";
|
||||
import "./note_tree.css";
|
||||
|
||||
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
|
||||
import type { SetNoteOpts } from "../components/note_context.js";
|
||||
import type { TouchBarItem } from "../components/touch_bar.js";
|
||||
import type FBranch from "../entities/fbranch.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { NoteType } from "../entities/fnote.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import type { TreeCommandNames } from "../menus/tree_context_menu.js";
|
||||
import branchService from "../services/branches.js";
|
||||
@@ -153,7 +153,7 @@ const TPL = /*html*/`
|
||||
const MAX_SEARCH_RESULTS_IN_TREE = 100;
|
||||
|
||||
// this has to be hanged on the actual elements to effectively intercept and stop click event
|
||||
const cancelClickPropagation: (e: JQuery.ClickEvent) => void = (e) => e.stopPropagation();
|
||||
const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation();
|
||||
|
||||
// TODO: Fix once we remove Node.js API from public
|
||||
type Timeout = NodeJS.Timeout | string | number | undefined;
|
||||
@@ -190,6 +190,9 @@ export interface DragData {
|
||||
|
||||
export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node";
|
||||
|
||||
/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */
|
||||
const BATCH_UPDATE_THRESHOLD = 10;
|
||||
|
||||
export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private $tree!: JQuery<HTMLElement>;
|
||||
private $treeActions!: JQuery<HTMLElement>;
|
||||
@@ -201,6 +204,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
private treeName: "main";
|
||||
private autoCollapseTimeoutId?: Timeout;
|
||||
private lastFilteredHoistedNotePath?: string | null;
|
||||
private spotlightedNotePath?: string | null;
|
||||
private spotlightedNode: Fancytree.FancytreeNode | null = null;
|
||||
private tree!: Fancytree.Fancytree;
|
||||
|
||||
constructor() {
|
||||
@@ -353,6 +358,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
this.$tree.fancytree({
|
||||
titlesTabbable: true,
|
||||
keyboard: true,
|
||||
toggleEffect: options.is("motionEnabled") ? undefined : false,
|
||||
extensions: ["dnd5", "clones", "filter"],
|
||||
source: treeData,
|
||||
scrollOfs: {
|
||||
@@ -552,7 +558,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else if (data.hitMode === "after") {
|
||||
branchService.moveAfterBranch(selectedBranchIds, node.data.branchId);
|
||||
} else if (data.hitMode === "over") {
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId);
|
||||
branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId);
|
||||
} else {
|
||||
throw new Error(`Unknown hitMode '${data.hitMode}'`);
|
||||
}
|
||||
@@ -598,102 +604,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
clones: {
|
||||
highlightActiveClones: true
|
||||
},
|
||||
async enhanceTitle (
|
||||
event: Event,
|
||||
data: {
|
||||
node: Fancytree.FancytreeNode;
|
||||
noteId: string;
|
||||
}
|
||||
) {
|
||||
const node = data.node;
|
||||
|
||||
if (!node.data.noteId) {
|
||||
// if there's "non-note" node, then don't enhance
|
||||
// this can happen for e.g. "Load error!" node
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await froca.getNote(node.data.noteId, true);
|
||||
|
||||
if (!note) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.find(".tree-item-button").remove();
|
||||
$span.find(".note-indicator-icon").remove();
|
||||
|
||||
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
|
||||
|
||||
if (note.hasLabel("workspace") && !isHoistedNote) {
|
||||
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($enterWorkspaceButton);
|
||||
}
|
||||
|
||||
if (note.type === "search") {
|
||||
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($refreshSearchButton);
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
|
||||
if (!["search", "launcher"].includes(note.type)
|
||||
&& !note.isOptions()
|
||||
&& !note.isLaunchBarConfig()
|
||||
&& !note.noteId.startsWith("_help")
|
||||
) {
|
||||
const $createChildNoteButton = $(`<span class="tree-item-button tn-icon add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($createChildNoteButton);
|
||||
}
|
||||
|
||||
if (isHoistedNote) {
|
||||
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
|
||||
// Add clone indicator with tooltip if note has multiple parents
|
||||
const parentNotes = note.getParentNotes();
|
||||
const realParents = parentNotes.filter(
|
||||
(parent) => !["_share", "_lbBookmarks"].includes(parent.noteId) && parent.type !== "search"
|
||||
);
|
||||
|
||||
if (realParents.length > 1) {
|
||||
const parentTitles = realParents.map((p) => p.title).join(", ");
|
||||
const tooltipText = realParents.length === 2
|
||||
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
|
||||
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
|
||||
|
||||
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
|
||||
$cloneIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($cloneIndicator);
|
||||
}
|
||||
|
||||
// Add shared indicator with tooltip if note is shared
|
||||
if (note.isShared()) {
|
||||
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
|
||||
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
|
||||
|
||||
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
|
||||
$sharedIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($sharedIndicator);
|
||||
}
|
||||
},
|
||||
enhanceTitle: buildEnhanceTitle(),
|
||||
// this is done to automatically lazy load all expanded notes after tree load
|
||||
loadChildren: (event, data) => {
|
||||
data.node.visit((subNode) => {
|
||||
@@ -803,6 +714,23 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE);
|
||||
}
|
||||
|
||||
if (parentNote.isLabelTruthy("subtreeHidden")) {
|
||||
// If we have a spotlighted note path, show only the child that leads to it
|
||||
if (this.spotlightedNotePath) {
|
||||
const spotlightPathSegments = this.spotlightedNotePath.split('/');
|
||||
const parentIndex = spotlightPathSegments.indexOf(parentNote.noteId);
|
||||
|
||||
if (parentIndex >= 0 && parentIndex < spotlightPathSegments.length - 1) {
|
||||
const nextNoteIdInPath = spotlightPathSegments[parentIndex + 1];
|
||||
childBranches = childBranches.filter(branch => branch.noteId === nextNoteIdInPath);
|
||||
} else {
|
||||
childBranches = [];
|
||||
}
|
||||
} else {
|
||||
childBranches = [];
|
||||
}
|
||||
}
|
||||
|
||||
for (const branch of childBranches) {
|
||||
if (hideArchivedNotes) {
|
||||
const note = branch.getNoteFromCache();
|
||||
@@ -874,6 +802,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
lazy: true,
|
||||
folder: isFolder,
|
||||
expanded: !!branch.isExpanded && note.type !== "search",
|
||||
subtreeHidden: note.isLabelTruthy("subtreeHidden"),
|
||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||
};
|
||||
|
||||
@@ -932,6 +861,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
extraClasses.push(...["tinted", colorClass]);
|
||||
}
|
||||
|
||||
if (this.spotlightedNotePath && this.spotlightedNotePath.endsWith(`/${note.noteId}`)) {
|
||||
extraClasses.push("spotlighted-node");
|
||||
}
|
||||
|
||||
return extraClasses.join(" ");
|
||||
}
|
||||
|
||||
@@ -1082,18 +1015,43 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
foundChildNode = this.findChildNode(parentNode, childNoteId);
|
||||
|
||||
if (!foundChildNode) {
|
||||
if (logErrors) {
|
||||
// besides real errors, this can be also caused by hiding of e.g. included images
|
||||
// these are real notes with real notePath, user can display them in a detail,
|
||||
// but they don't have a node in the tree
|
||||
const childNote = await froca.getNote(childNoteId);
|
||||
|
||||
const childNote = await froca.getNote(childNoteId);
|
||||
if (childNote?.type === "image") return;
|
||||
|
||||
if (!childNote || childNote.type !== "image") {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
// The child note can be part of a note with #subtreeHidden, case in which we need to "spotlight" it.
|
||||
const parentNote = froca.getNoteFromCache(parentNode.data.noteId);
|
||||
if (parentNote?.isLabelTruthy("subtreeHidden")) {
|
||||
// Enable spotlight mode and reload the parent to show only the path to this note
|
||||
this.spotlightedNotePath = notePath;
|
||||
await parentNode.load(true);
|
||||
|
||||
// Try to find the child again after reload
|
||||
foundChildNode = this.findChildNode(parentNode, childNoteId);
|
||||
this.spotlightedNode = foundChildNode ?? null;
|
||||
|
||||
if (!foundChildNode) {
|
||||
if (logErrors || !childNote) {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
parentNode = foundChildNode;
|
||||
continue;
|
||||
}
|
||||
|
||||
// besides real errors, this can be also caused by hiding of e.g. included images
|
||||
// these are real notes with real notePath, user can display them in a detail,
|
||||
// but they don't have a node in the tree
|
||||
if (logErrors || !childNote) {
|
||||
ws.logError(
|
||||
`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1108,7 +1066,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
findChildNode(parentNode: Fancytree.FancytreeNode, childNoteId: string) {
|
||||
return parentNode.getChildren().find((childNode) => childNode.data.noteId === childNoteId);
|
||||
return parentNode.getChildren()?.find((childNode) => childNode.data.noteId === childNoteId);
|
||||
}
|
||||
|
||||
async expandToNote(notePath: string, logErrors = true) {
|
||||
@@ -1149,12 +1107,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
(!treeService.isNotePathInHiddenSubtree(this.noteContext.notePath) || (await hoistedNoteService.isHoistedInHiddenSubtree())) &&
|
||||
(await this.getNodeFromPath(this.noteContext.notePath));
|
||||
|
||||
if (this.spotlightedNode && newActiveNode !== this.spotlightedNode) {
|
||||
// Can get removed when switching to another note in a spotlighted subtree.
|
||||
if (this.spotlightedNode.parent) {
|
||||
this.spotlightedNode.remove();
|
||||
}
|
||||
this.spotlightedNode = null;
|
||||
this.spotlightedNotePath = null;
|
||||
}
|
||||
|
||||
if (newActiveNode !== oldActiveNode) {
|
||||
let oldActiveNodeFocused = false;
|
||||
|
||||
if (oldActiveNode) {
|
||||
oldActiveNodeFocused = oldActiveNode.hasFocus();
|
||||
|
||||
oldActiveNode.setActive(false);
|
||||
oldActiveNode.setFocus(false);
|
||||
}
|
||||
@@ -1257,10 +1223,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx);
|
||||
|
||||
for (const noteId of loadResults.getNoteIds()) {
|
||||
const contentReloaded = loadResults.isNoteContentReloaded(noteId);
|
||||
if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) {
|
||||
// Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note.
|
||||
continue;
|
||||
}
|
||||
|
||||
refreshCtx.noteIdsToUpdate.add(noteId);
|
||||
}
|
||||
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) {
|
||||
await this.#executeTreeUpdates(refreshCtx, loadResults);
|
||||
}
|
||||
|
||||
await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes);
|
||||
|
||||
@@ -1280,7 +1254,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else {
|
||||
refreshCtx.noteIdsToUpdate.add(attrRow.noteId);
|
||||
}
|
||||
} else if (attrRow.type === "label" && attrRow.name === "archived" && attrRow.noteId) {
|
||||
} else if (attrRow.type === "label" && (attrRow.name === "archived" || attrRow.name === "subtreeHidden") && attrRow.noteId) {
|
||||
const note = froca.getNoteFromCache(attrRow.noteId);
|
||||
|
||||
if (note) {
|
||||
@@ -1365,18 +1339,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
} else if (frocaBranch) {
|
||||
// make sure it's loaded
|
||||
// we're forcing lazy since it's not clear if the whole required subtree is in froca
|
||||
const newNode = this.prepareNode(frocaBranch, true);
|
||||
if (newNode) {
|
||||
parentNode.addChildren([newNode]);
|
||||
}
|
||||
if (!parentNode.data.subtreeHidden) {
|
||||
const newNode = this.prepareNode(frocaBranch, true);
|
||||
if (newNode) {
|
||||
parentNode.addChildren([newNode]);
|
||||
}
|
||||
|
||||
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
|
||||
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
|
||||
}
|
||||
if (frocaBranch?.isExpanded && note && note.hasChildren()) {
|
||||
refreshCtx.noteIdsToReload.add(frocaBranch.noteId);
|
||||
}
|
||||
|
||||
this.sortChildren(parentNode);
|
||||
this.sortChildren(parentNode);
|
||||
} else if (branchRow.componentId === this.componentId) {
|
||||
// Display the toast and focus to parent note only if we know for sure that the operation comes from the tree.
|
||||
const parentNote = froca.getNoteFromCache(parentNode.data.noteId || "");
|
||||
toastService.showPersistent({
|
||||
id: `subtree-hidden-moved`,
|
||||
title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }),
|
||||
message: parentNote?.type === "book"
|
||||
? t("note_tree.subtree-hidden-moved-description-collection")
|
||||
: t("note_tree.subtree-hidden-moved-description-other"),
|
||||
icon: "bx bx-hide",
|
||||
timeout: 5_000,
|
||||
});
|
||||
parentNode.setActive(true);
|
||||
}
|
||||
|
||||
// this might be a first child which would force an icon change
|
||||
// also update the count if the subtree is hidden.
|
||||
if (branchRow.parentNoteId) {
|
||||
refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId);
|
||||
}
|
||||
@@ -1392,7 +1382,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) {
|
||||
await this.batchUpdate(async () => {
|
||||
const performUpdates = async () => {
|
||||
for (const noteId of refreshCtx.noteIdsToReload) {
|
||||
for (const node of this.getNodesByNoteId(noteId)) {
|
||||
await node.load(true);
|
||||
@@ -1408,7 +1398,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) {
|
||||
/**
|
||||
* Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title)
|
||||
* it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates.
|
||||
* Without batching, the updates would take a couple of milliseconds.
|
||||
* We still keep the batching for potential cases where there are many updates, for example in a sync.
|
||||
*/
|
||||
await this.batchUpdate(performUpdates);
|
||||
} else {
|
||||
await performUpdates();
|
||||
}
|
||||
|
||||
// for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered)
|
||||
for (const noteId of refreshCtx.noteIdsToUpdate) {
|
||||
@@ -1672,7 +1674,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const toNode = node.getPrevSibling();
|
||||
|
||||
if (toNode !== null) {
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId);
|
||||
branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1809,12 +1811,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
#moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) {
|
||||
const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile"));
|
||||
if (desktopLaunchersToMove) {
|
||||
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`);
|
||||
branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId);
|
||||
}
|
||||
|
||||
const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile"));
|
||||
if (mobileLaunchersToMove) {
|
||||
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`);
|
||||
branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1882,3 +1884,112 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
function buildEnhanceTitle() {
|
||||
const createChildTemplate = document.createElement("span");
|
||||
createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus";
|
||||
createChildTemplate.title = t("note_tree.create-child-note");
|
||||
|
||||
return async function enhanceTitle(event: Event,
|
||||
data: {
|
||||
node: Fancytree.FancytreeNode;
|
||||
noteId: string;
|
||||
}) {
|
||||
const node = data.node;
|
||||
|
||||
if (!node.data.noteId) {
|
||||
// if there's "non-note" node, then don't enhance
|
||||
// this can happen for e.g. "Load error!" node
|
||||
return;
|
||||
}
|
||||
|
||||
const note = froca.getNoteFromCache(node.data.noteId);
|
||||
if (!note) return;
|
||||
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
|
||||
const $span = $(node.span);
|
||||
|
||||
$span.find(".tree-item-button").remove();
|
||||
$span.find(".note-indicator-icon").remove();
|
||||
|
||||
const isHoistedNote = activeNoteContext && activeNoteContext.hoistedNoteId === note.noteId && note.noteId !== "root";
|
||||
|
||||
if (note.hasLabel("workspace") && !isHoistedNote) {
|
||||
const $enterWorkspaceButton = $(`<span class="tree-item-button tn-icon enter-workspace-button bx bx-door-open" title="${t("note_tree.hoist-this-note-workspace")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($enterWorkspaceButton);
|
||||
}
|
||||
|
||||
if (note.type === "search") {
|
||||
const $refreshSearchButton = $(`<span class="tree-item-button tn-icon refresh-search-button bx bx-refresh" title="${t("note_tree.refresh-saved-search-results")}"></span>`).on(
|
||||
"click",
|
||||
cancelClickPropagation
|
||||
);
|
||||
|
||||
$span.append($refreshSearchButton);
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server's notes.ts#getAndValidateParent
|
||||
const isSubtreeHidden = note.isLabelTruthy("subtreeHidden");
|
||||
if (!["search", "launcher"].includes(note.type)
|
||||
&& !note.isOptions()
|
||||
&& !note.isLaunchBarConfig()
|
||||
&& !note.noteId.startsWith("_help")
|
||||
&& !isSubtreeHidden
|
||||
&& !node.extraClasses.includes("spotlighted-node")
|
||||
) {
|
||||
const createChildItem = createChildTemplate.cloneNode();
|
||||
createChildItem.addEventListener("click", cancelClickPropagation);
|
||||
node.span.append(createChildItem);
|
||||
}
|
||||
|
||||
if (isHoistedNote) {
|
||||
const $unhoistButton = $(`<span class="tree-item-button tn-icon unhoist-button bx bx-door-open" title="${t("note_tree.unhoist")}"></span>`).on("click", cancelClickPropagation);
|
||||
|
||||
$span.append($unhoistButton);
|
||||
}
|
||||
|
||||
// Add clone indicator with tooltip if note has multiple parents
|
||||
const parentNotes = note.getParentNotes();
|
||||
const realParents: FNote[] = [];
|
||||
for (const parent of parentNotes) {
|
||||
if (parent.noteId !== "_share" && parent.noteId !== "_lbBookmarks" && parent.type !== "search") {
|
||||
realParents.push(parent);
|
||||
}
|
||||
}
|
||||
|
||||
if (realParents.length > 1) {
|
||||
const parentTitles = realParents.map((p) => p.title).join(", ");
|
||||
const tooltipText = realParents.length === 2
|
||||
? t("note_tree.clone-indicator-tooltip-single", { parent: realParents[1].title })
|
||||
: t("note_tree.clone-indicator-tooltip", { count: realParents.length, parents: parentTitles });
|
||||
|
||||
const $cloneIndicator = $(`<span class="note-indicator-icon clone-indicator"></span>`);
|
||||
$cloneIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($cloneIndicator);
|
||||
}
|
||||
|
||||
// Add shared indicator with tooltip if note is shared
|
||||
if (note.isShared()) {
|
||||
const shareId = note.getOwnedLabelValue("shareAlias") || note.noteId;
|
||||
const shareUrl = `${location.origin}${location.pathname}share/${shareId}`;
|
||||
const tooltipText = t("note_tree.shared-indicator-tooltip-with-url", { url: shareUrl });
|
||||
|
||||
const $sharedIndicator = $(`<span class="note-indicator-icon shared-indicator"></span>`);
|
||||
$sharedIndicator.attr("title", tooltipText);
|
||||
$span.find(".fancytree-title").append($sharedIndicator);
|
||||
}
|
||||
|
||||
// Add a badge with the number of items if it hides children.
|
||||
const count = note.getChildNoteIds().length;
|
||||
if (isSubtreeHidden && count > 0) {
|
||||
const $badge = $(`<span class="note-indicator-icon subtree-hidden-badge">${count}</span>`);
|
||||
$badge.attr("title", t("note_tree.subtree-hidden-tooltip", { count }));
|
||||
$span.find(".fancytree-title").append($badge);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -646,17 +646,13 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: F
|
||||
|
||||
const setter = useCallback((value: boolean) => {
|
||||
if (note) {
|
||||
if (value) {
|
||||
attributes.setLabel(note.noteId, labelName, "");
|
||||
} else {
|
||||
attributes.removeOwnedLabelByName(note, labelName);
|
||||
}
|
||||
attributes.setBooleanWithInheritance(note, labelName, value);
|
||||
}
|
||||
}, [note]);
|
||||
}, [note, labelName]);
|
||||
|
||||
useDebugValue(labelName);
|
||||
|
||||
const labelValue = !!note?.hasLabel(labelName);
|
||||
const labelValue = !!note?.isLabelTruthy(labelName);
|
||||
return [ labelValue, setter ] as const;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,14 @@ import "./TableOfContents.css";
|
||||
|
||||
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
|
||||
import clsx from "clsx";
|
||||
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, useGetContextData, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import RawHtml from "../react/RawHtml";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
//#region Generic impl.
|
||||
@@ -80,6 +82,22 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
|
||||
}) {
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
const isActive = heading.id === activeHeadingId;
|
||||
const contentRef = useRef<HTMLElement>(null);
|
||||
|
||||
// Render math equations after component mounts/updates
|
||||
useEffect(() => {
|
||||
if (!contentRef.current) return;
|
||||
const mathElements = contentRef.current.querySelectorAll(".ck-math-tex");
|
||||
|
||||
for (const mathEl of mathElements ?? []) {
|
||||
try {
|
||||
math.render(mathEl.textContent || "", mathEl as HTMLElement);
|
||||
} catch (e) {
|
||||
console.warn("Failed to render math in TOC:", e);
|
||||
}
|
||||
}
|
||||
}, [heading.text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<li className={clsx(collapsed && "collapsed", isActive && "active")}>
|
||||
@@ -90,12 +108,14 @@ function TableOfContentsHeading({ heading, scrollToHeading, activeHeadingId }: {
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
<RawHtml
|
||||
containerRef={contentRef}
|
||||
className="item-content"
|
||||
onClick={() => scrollToHeading(heading)}
|
||||
>{heading.text}</span>
|
||||
html={heading.text}
|
||||
/>
|
||||
</li>
|
||||
{heading.children && (
|
||||
{heading.children.length > 0 && (
|
||||
<ol>
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} activeHeadingId={activeHeadingId} />)}
|
||||
</ol>
|
||||
@@ -189,9 +209,23 @@ function extractTocFromTextEditor(editor: CKTextEditor) {
|
||||
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
|
||||
|
||||
const level = Number(item.name.replace( 'heading', '' ));
|
||||
const text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
|
||||
// Convert model element to view, then to DOM to get HTML
|
||||
const viewEl = editor.editing.mapper.toViewElement(item);
|
||||
let text = '';
|
||||
if (viewEl) {
|
||||
const domEl = editor.editing.view.domConverter.mapViewToDom(viewEl);
|
||||
if (domEl instanceof HTMLElement) {
|
||||
text = domEl.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to plain text if conversion fails
|
||||
if (!text) {
|
||||
text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
}
|
||||
|
||||
// Assign a unique ID
|
||||
let tocId = item.getAttribute(TOC_ID) as string | undefined;
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function SqlResults() {
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div class="sql-console-result-container">
|
||||
<div className="sql-console-result-container selectable-text">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import "./Image.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TransformComponent,TransformWrapper } from "react-zoom-pan-pinch";
|
||||
|
||||
import image_context_menu from "../../menus/image_context_menu";
|
||||
import { copyImageReferenceToClipboard } from "../../services/image";
|
||||
import { createImageSrcUrl } from "../../services/utils";
|
||||
import { useTriliumEvent, useUniqueName } from "../react/hooks";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import "./Image.css";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
import WheelZoom from 'vanilla-js-wheel-zoom';
|
||||
import image_context_menu from "../../menus/image_context_menu";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { copyImageReferenceToClipboard } from "../../services/image";
|
||||
|
||||
export default function Image({ note, ntxId }: TypeWidgetProps) {
|
||||
const uniqueId = useUniqueName("image");
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||
|
||||
// Set up pan & zoom
|
||||
useEffect(() => {
|
||||
const zoomInstance = WheelZoom.create(`#${uniqueId}`, {
|
||||
maxScale: 50,
|
||||
speed: 1.3,
|
||||
zoomOnClick: false
|
||||
});
|
||||
|
||||
return () => zoomInstance.destroy();
|
||||
}, [ note ]);
|
||||
|
||||
// Set up context menu
|
||||
useEffect(() => image_context_menu.setupContextMenu(refToJQuerySelector(containerRef)), []);
|
||||
|
||||
@@ -33,23 +42,11 @@ export default function Image({ note, ntxId }: TypeWidgetProps) {
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="note-detail-image-wrapper">
|
||||
<TransformWrapper
|
||||
initialScale={1}
|
||||
centerOnInit
|
||||
>
|
||||
<TransformComponent
|
||||
wrapperStyle={{
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
id={uniqueId}
|
||||
className="note-detail-image-view"
|
||||
src={createImageSrcUrl(note)}
|
||||
/>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
<img
|
||||
id={uniqueId}
|
||||
className="note-detail-image-view"
|
||||
src={createImageSrcUrl(note)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
.note-detail-split .note-detail-split-editor {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.note-detail-split .note-detail-split-editor .note-detail-code {
|
||||
@@ -30,6 +32,7 @@
|
||||
margin: 5px;
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.85em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.note-detail-split .note-detail-split-preview {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { RefObject } from "preact";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import toast from "../../../services/toast";
|
||||
import utils from "../../../services/utils";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import SplitEditor, { PreviewButton, SplitEditorProps } from "./SplitEditor";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import server from "../../../services/server";
|
||||
import svgPanZoom from "svg-pan-zoom";
|
||||
import { RefObject } from "preact";
|
||||
import { useElementSize, useTriliumEvent } from "../../react/hooks";
|
||||
import utils from "../../../services/utils";
|
||||
import toast from "../../../services/toast";
|
||||
|
||||
interface SvgSplitEditorProps extends Omit<SplitEditorProps, "previewContent"> {
|
||||
/**
|
||||
@@ -119,20 +117,11 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
onContentChanged={onContentChanged}
|
||||
dataSaved={onSave}
|
||||
previewContent={(
|
||||
<TransformWrapper>
|
||||
<TransformComponent
|
||||
wrapperStyle={{
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<RawHtmlBlock
|
||||
className="render-container"
|
||||
containerRef={containerRef}
|
||||
html={svg}
|
||||
/>
|
||||
</TransformComponent>
|
||||
</TransformWrapper>
|
||||
<RawHtmlBlock
|
||||
className="render-container"
|
||||
containerRef={containerRef}
|
||||
html={svg}
|
||||
/>
|
||||
)}
|
||||
previewButtons={
|
||||
<>
|
||||
@@ -155,7 +144,7 @@ export default function SvgSplitEditor({ ntxId, note, attachmentName, renderSvg,
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg: string | undefined) {
|
||||
@@ -192,7 +181,7 @@ function useResizer(containerRef: RefObject<HTMLDivElement>, noteId: string, svg
|
||||
lastPanZoom.current = {
|
||||
pan: zoomInstance.getPan(),
|
||||
zoom: zoomInstance.getZoom()
|
||||
};
|
||||
}
|
||||
zoomRef.current = undefined;
|
||||
zoomInstance.destroy();
|
||||
};
|
||||
|
||||
@@ -286,7 +286,7 @@ function useWatchdogCrashHandling() {
|
||||
const currentState = watchdog.state;
|
||||
logInfo(`CKEditor state changed to ${currentState}`);
|
||||
|
||||
if (currentState === "ready") {
|
||||
if (currentState === "ready" && hasCrashed.current) {
|
||||
hasCrashed.current = false;
|
||||
watchdog.editor?.focus();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO } from "@triliumnext/commons";
|
||||
import { buildExtraCommands, type EditorConfig, getCkLocale, PREMIUM_PLUGINS, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||
import options from "../../../services/options.js";
|
||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
import { buildExtraCommands, type EditorConfig, getCkLocale, loadPremiumPlugins, TemplateDefinition } from "@triliumnext/ckeditor5";
|
||||
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/src/emoji_definitions/en.json?url";
|
||||
import { ALLOWED_PROTOCOLS, DISPLAYABLE_LOCALE_IDS, MIME_TYPE_AUTO, normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
|
||||
import { copyTextWithToast } from "../../../services/clipboard_ext.js";
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import { getMermaidConfig } from "../../../services/mermaid.js";
|
||||
import { default as mimeTypesService, getHighlightJsNameForMime } from "../../../services/mime_types.js";
|
||||
import noteAutocompleteService, { type Suggestion } from "../../../services/note_autocomplete.js";
|
||||
import mimeTypesService from "../../../services/mime_types.js";
|
||||
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||
import options from "../../../services/options.js";
|
||||
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
import { buildToolbarConfig } from "./toolbar.js";
|
||||
|
||||
export const OPEN_SOURCE_LICENSE_KEY = "GPL";
|
||||
@@ -36,7 +35,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
engine: "katex",
|
||||
outputType: "span", // or script
|
||||
lazyLoad: async () => {
|
||||
(window as any).katex = (await import("../../../services/math.js")).default
|
||||
(window as any).katex = (await import("../../../services/math.js")).default;
|
||||
},
|
||||
forceOutputType: false, // forces output to use outputType
|
||||
enablePreview: true // Enable preview view
|
||||
@@ -172,7 +171,7 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
config.language = {
|
||||
ui: (typeof config.language === "string" ? config.language : "en"),
|
||||
content: contentLanguage
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Mention customisation.
|
||||
@@ -195,11 +194,9 @@ export async function buildConfig(opts: BuildEditorOptions): Promise<EditorConfi
|
||||
};
|
||||
}
|
||||
|
||||
// Enable premium plugins.
|
||||
// Enable premium plugins dynamically to avoid eager loading.
|
||||
if (hasPremiumLicense) {
|
||||
config.extraPlugins = [
|
||||
...PREMIUM_PLUGINS
|
||||
];
|
||||
config.extraPlugins = await loadPremiumPlugins();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -237,7 +234,7 @@ function getLicenseKey() {
|
||||
}
|
||||
|
||||
function getDisabledPlugins() {
|
||||
let disabledPlugins: string[] = [];
|
||||
const disabledPlugins: string[] = [];
|
||||
|
||||
if (options.get("textNoteEmojiCompletionEnabled") !== "true") {
|
||||
disabledPlugins.push("EmojiMention");
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
/// <reference types='vitest' />
|
||||
import { join, resolve } from 'path';
|
||||
import { defineConfig, type Plugin } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
import prefresh from '@prefresh/vite';
|
||||
import { join } from 'path';
|
||||
import webpackStatsPlugin from 'rollup-plugin-webpack-stats';
|
||||
import preact from "@preact/preset-vite";
|
||||
import { defineConfig } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
|
||||
const assets = [ "assets", "stylesheets", "fonts", "translations" ];
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
let plugins: any = [
|
||||
preact({
|
||||
babel: {
|
||||
compact: !isDev
|
||||
}
|
||||
})
|
||||
];
|
||||
let plugins: any = [];
|
||||
|
||||
if (!isDev) {
|
||||
if (isDev) {
|
||||
// Add Prefresh for Preact HMR in development
|
||||
plugins = [
|
||||
prefresh()
|
||||
];
|
||||
} else {
|
||||
plugins = [
|
||||
...plugins,
|
||||
viteStaticCopy({
|
||||
targets: assets.map((asset) => ({
|
||||
src: `src/${asset}/*`,
|
||||
@@ -40,9 +38,19 @@ if (!isDev) {
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/client',
|
||||
cacheDir: '../../.cache/vite',
|
||||
base: "",
|
||||
plugins,
|
||||
// Use esbuild for JSX transformation (much faster than Babel)
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
jsxImportSource: 'preact',
|
||||
jsxDev: isDev
|
||||
},
|
||||
css: {
|
||||
transformer: 'lightningcss',
|
||||
devSourcemap: isDev
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
@@ -62,6 +70,13 @@ export default defineConfig(() => ({
|
||||
"preact/hooks"
|
||||
]
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
"ckeditor5-premium-features",
|
||||
"ckeditor5",
|
||||
"mathlive"
|
||||
]
|
||||
},
|
||||
build: {
|
||||
target: "esnext",
|
||||
outDir: './dist',
|
||||
@@ -70,8 +85,7 @@ export default defineConfig(() => ({
|
||||
sourcemap: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
desktop: join(__dirname, "src", "desktop.ts"),
|
||||
mobile: join(__dirname, "src", "mobile.ts"),
|
||||
index: join(__dirname, "src", "index.html"),
|
||||
login: join(__dirname, "src", "login.ts"),
|
||||
setup: join(__dirname, "src", "setup.ts"),
|
||||
set_password: join(__dirname, "src", "set_password.ts"),
|
||||
@@ -80,11 +94,10 @@ export default defineConfig(() => ({
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "src/[name].js",
|
||||
chunkFileNames: "src/[name].js",
|
||||
assetFileNames: "src/[name].[ext]",
|
||||
chunkFileNames: "src/[name]-[hash].js",
|
||||
assetFileNames: "src/[name]-[hash].[ext]",
|
||||
manualChunks: {
|
||||
"ckeditor5": [ "@triliumnext/ckeditor5" ],
|
||||
"boxicons": [ "../../node_modules/boxicons/css/boxicons.min.css" ]
|
||||
"ckeditor5": [ "@triliumnext/ckeditor5" ]
|
||||
},
|
||||
},
|
||||
onwarn(warning, rollupWarn) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "Tool to compare content of Trilium databases. Useful for debugging sync problems.",
|
||||
"dependencies": {
|
||||
"colors": "1.4.0",
|
||||
"diff": "8.0.2",
|
||||
"diff": "8.0.3",
|
||||
"sqlite": "5.1.1",
|
||||
"sqlite3": "5.1.7"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/desktop",
|
||||
"version": "0.101.1",
|
||||
"version": "0.101.3",
|
||||
"description": "Build your personal knowledge base with Trilium Notes",
|
||||
"private": true,
|
||||
"main": "src/main.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "2.1.3",
|
||||
"better-sqlite3": "12.5.0",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"electron-debug": "4.1.0",
|
||||
"electron-dl": "4.0.0",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
@@ -36,14 +36,14 @@
|
||||
"@triliumnext/server": "workspace:*",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"electron": "39.2.7",
|
||||
"@electron-forge/cli": "7.10.2",
|
||||
"@electron-forge/maker-deb": "7.10.2",
|
||||
"@electron-forge/maker-dmg": "7.10.2",
|
||||
"@electron-forge/maker-flatpak": "7.10.2",
|
||||
"@electron-forge/maker-rpm": "7.10.2",
|
||||
"@electron-forge/maker-squirrel": "7.10.2",
|
||||
"@electron-forge/maker-zip": "7.10.2",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.10.2",
|
||||
"@electron-forge/cli": "7.11.1",
|
||||
"@electron-forge/maker-deb": "7.11.1",
|
||||
"@electron-forge/maker-dmg": "7.11.1",
|
||||
"@electron-forge/maker-flatpak": "7.11.1",
|
||||
"@electron-forge/maker-rpm": "7.11.1",
|
||||
"@electron-forge/maker-squirrel": "7.11.1",
|
||||
"@electron-forge/maker-zip": "7.11.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
|
||||
"prebuild-install": "7.1.3"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 545 B |
|
After Width: | Height: | Size: 727 B |
|
After Width: | Height: | Size: 828 B |
|
After Width: | Height: | Size: 931 B |
BIN
apps/desktop/src/assets/images/tray/closed-windowsTemplate.png
Normal file
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 355 B |
|
After Width: | Height: | Size: 434 B |
|
After Width: | Height: | Size: 492 B |
@@ -6,6 +6,7 @@ import sqlInit from "@triliumnext/server/src/services/sql_init.js";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import tray from "@triliumnext/server/src/services/tray.js";
|
||||
import options from "@triliumnext/server/src/services/options.js";
|
||||
|
||||
import electronDebug from "electron-debug";
|
||||
import electronDl from "electron-dl";
|
||||
import { PRODUCT_NAME } from "./app-info";
|
||||
@@ -69,10 +70,12 @@ async function main() {
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
app.on("second-instance", (event, commandLine) => {
|
||||
app.on("second-instance", async (event, commandLine) => {
|
||||
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
||||
if (commandLine.includes("--new-window")) {
|
||||
windowService.createExtraWindow("");
|
||||
const randomString = (await import("@triliumnext/server/src/services/utils.js")).randomString;
|
||||
const extraWindowId = randomString(4);
|
||||
windowService.createExtraWindow(extraWindowId, "");
|
||||
} else if (lastFocusedWindow) {
|
||||
if (lastFocusedWindow.isMinimized()) {
|
||||
lastFocusedWindow.restore();
|
||||
@@ -124,7 +127,8 @@ async function onReady() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
await normalizeOpenNoteContexts();
|
||||
tray.createTray();
|
||||
} else {
|
||||
await windowService.createSetupWindow();
|
||||
@@ -133,6 +137,30 @@ async function onReady() {
|
||||
await windowService.registerGlobalShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Some windows may have closed abnormally, leaving closedAt as 0 in openNoteContexts.
|
||||
* This function normalizes those timestamps to the current time for correct sorting/filtering.
|
||||
*/
|
||||
async function normalizeOpenNoteContexts() {
|
||||
const savedWindows = options.getOptionJson("openNoteContexts") || [];
|
||||
const now = Date.now();
|
||||
|
||||
let changed = false;
|
||||
for (const win of savedWindows) {
|
||||
if (win.windowId !== "main" && win.closedAt === 0) {
|
||||
win.closedAt = now;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
const { default: cls } = (await import("@triliumnext/server/src/services/cls.js"));
|
||||
cls.wrap(() => {
|
||||
options.setOption("openNoteContexts", JSON.stringify(savedWindows));
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
function getElectronLocale() {
|
||||
const uiLocale = options.getOptionOrNull("locale");
|
||||
const formattingLocale = options.getOptionOrNull("formattingLocale");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Standalone tool to dump contents of Trilium document.db file into a directory tree of notes",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.5.0",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"mime-types": "3.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"tsx": "4.21.0",
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@triliumnext/edit-docs",
|
||||
"version": "0.0.1",
|
||||
"version": "0.101.3",
|
||||
"private": true,
|
||||
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
|
||||
"dependencies": {
|
||||
"archiver": "7.0.1",
|
||||
"better-sqlite3": "12.5.0"
|
||||
"better-sqlite3": "12.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@triliumnext/client": "workspace:*",
|
||||
@@ -16,7 +16,9 @@
|
||||
"fs-extra": "11.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"test-build": "vitest --config vitest.build.config.mts",
|
||||
"edit-docs": "cross-env TRILIUM_PORT=37741 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store tsx ../../scripts/electron-start.mts src/edit-docs.ts",
|
||||
"edit-demo": "cross-env TRILIUM_PORT=37744 TRILIUM_DATA_DIR=data TRILIUM_INTEGRATION_TEST=memory-no-store DOCS_ROOT=../../../docs USER_GUIDE_ROOT=\"../../server/src/assets/doc_notes/en/User Guide\" tsx ../../scripts/electron-start.mts src/edit-demo.ts"
|
||||
}
|
||||
}
|
||||
40
apps/edit-docs/scripts/build.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
import BuildHelper from "../../../scripts/build-utils";
|
||||
import originalPackageJson from "../package.json" with { type: "json" };
|
||||
|
||||
const build = new BuildHelper("apps/edit-docs");
|
||||
|
||||
async function main() {
|
||||
await build.buildBackend(["src/edit-docs.ts", "src/utils.ts"]);
|
||||
|
||||
// Copy assets from server (needed for DB initialization)
|
||||
build.copy("/apps/server/src/assets", "assets/");
|
||||
build.triggerBuildAndCopyTo("packages/share-theme", "share-theme/assets/");
|
||||
build.copy("/packages/share-theme/src/templates", "share-theme/templates/");
|
||||
build.copy("/node_modules/ckeditor5/dist/ckeditor5-content.css", "ckeditor5-content.css");
|
||||
build.buildFrontend();
|
||||
|
||||
// Copy node modules dependencies
|
||||
build.copyNodeModules(["better-sqlite3", "bindings", "file-uri-to-path", "@electron/remote"]);
|
||||
|
||||
generatePackageJson();
|
||||
}
|
||||
|
||||
function generatePackageJson() {
|
||||
const { version, author, license, description, dependencies, devDependencies } = originalPackageJson;
|
||||
const packageJson = {
|
||||
name: "trilium-edit-docs",
|
||||
main: "edit-docs.cjs",
|
||||
version,
|
||||
author,
|
||||
license,
|
||||
description,
|
||||
dependencies: {"better-sqlite3": dependencies["better-sqlite3"]},
|
||||
devDependencies: {electron: devDependencies.electron},
|
||||
};
|
||||
writeFileSync(join(build.outDir, "package.json"), JSON.stringify(packageJson, null, "\t"), "utf-8");
|
||||
}
|
||||
|
||||
main();
|
||||
48
apps/edit-docs/spec/build-checks/artifacts.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { globSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { it, describe, expect } from "vitest";
|
||||
|
||||
describe("Check artifacts are present", () => {
|
||||
const distPath = join(__dirname, "../../dist");
|
||||
|
||||
it("has the necessary node modules", async () => {
|
||||
const paths = [
|
||||
"node_modules/better-sqlite3",
|
||||
"node_modules/bindings",
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/@electron/remote"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
it("includes the client", async () => {
|
||||
const paths = [
|
||||
"public/assets",
|
||||
"public/fonts",
|
||||
"public/node_modules",
|
||||
"public/src",
|
||||
"public/stylesheets",
|
||||
"public/translations"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
it("includes necessary assets", async () => {
|
||||
const paths = [
|
||||
"assets",
|
||||
"share-theme",
|
||||
"ckeditor5-content.css"
|
||||
];
|
||||
|
||||
ensurePathsExist(paths);
|
||||
});
|
||||
|
||||
function ensurePathsExist(paths: string[]) {
|
||||
for (const path of paths) {
|
||||
const result = globSync(join(distPath, path, "**"));
|
||||
expect(result, path).not.toHaveLength(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,14 +1,17 @@
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import path from "path";
|
||||
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
|
||||
import debounce from "@triliumnext/client/src/services/debounce.js";
|
||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import type { AdvancedExportOptions, ExportFormat } from "@triliumnext/server/src/services/export/zip/abstract_provider.js";
|
||||
import { initializeTranslations } from "@triliumnext/server/src/services/i18n.js";
|
||||
import { parseNoteMetaFile } from "@triliumnext/server/src/services/in_app_help.js";
|
||||
import type { NoteMetaFile } from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import type NoteMeta from "@triliumnext/server/src/services/meta/note_meta.js";
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
import { extractZip, importData, initializeDatabase, startElectron } from "./utils.js";
|
||||
|
||||
interface NoteMapping {
|
||||
rootNoteId: string;
|
||||
@@ -18,47 +21,113 @@ interface NoteMapping {
|
||||
exportOnly?: boolean;
|
||||
}
|
||||
|
||||
const { DOCS_ROOT, USER_GUIDE_ROOT } = process.env;
|
||||
if (!DOCS_ROOT || !USER_GUIDE_ROOT) {
|
||||
throw new Error("Missing DOCS_ROOT or USER_GUIDE_ROOT environment variable.");
|
||||
interface Config {
|
||||
baseUrl: string;
|
||||
noteMappings: NoteMapping[];
|
||||
}
|
||||
|
||||
const BASE_URL = "https://docs.triliumnotes.org";
|
||||
// Parse command-line arguments
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
let configPath: string | undefined;
|
||||
let showHelp = false;
|
||||
let showVersion = false;
|
||||
|
||||
const NOTE_MAPPINGS: NoteMapping[] = [
|
||||
{
|
||||
rootNoteId: "pOsGYCXsbNQG",
|
||||
path: path.join(__dirname, DOCS_ROOT, "User Guide"),
|
||||
format: "markdown"
|
||||
},
|
||||
{
|
||||
rootNoteId: "pOsGYCXsbNQG",
|
||||
path: path.join(__dirname, USER_GUIDE_ROOT),
|
||||
format: "html",
|
||||
ignoredFiles: ["index.html", "navigation.html", "style.css", "User Guide.html"],
|
||||
exportOnly: true
|
||||
},
|
||||
{
|
||||
rootNoteId: "jdjRLhLV3TtI",
|
||||
path: path.join(__dirname, DOCS_ROOT, "Developer Guide"),
|
||||
format: "markdown"
|
||||
},
|
||||
{
|
||||
rootNoteId: "hD3V4hiu2VW4",
|
||||
path: path.join(__dirname, DOCS_ROOT, "Release Notes"),
|
||||
format: "markdown"
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--config' || args[i] === '-c') {
|
||||
configPath = args[i + 1];
|
||||
if (!configPath) {
|
||||
console.error("Error: --config/-c requires a path argument");
|
||||
process.exit(1);
|
||||
}
|
||||
i++; // Skip the next argument as it's the value
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
showHelp = true;
|
||||
} else if (args[i] === '--version' || args[i] === '-v') {
|
||||
showVersion = true;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return { configPath, showHelp, showVersion };
|
||||
}
|
||||
|
||||
function getVersion(): string {
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
const version = getVersion();
|
||||
console.log(`
|
||||
Usage: trilium-edit-docs [options]
|
||||
|
||||
Options:
|
||||
-c, --config <path> Path to the configuration file (default: edit-docs-config.yaml in the root)
|
||||
-h, --help Display this help message
|
||||
-v, --version Display version information
|
||||
|
||||
Version: ${version}
|
||||
`);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
const version = getVersion();
|
||||
console.log(version);
|
||||
}
|
||||
|
||||
const { configPath, showHelp, showVersion } = parseArgs();
|
||||
|
||||
if (showHelp) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
} else if (showVersion) {
|
||||
printVersion();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Configuration variables to be initialized
|
||||
let BASE_URL: string;
|
||||
let NOTE_MAPPINGS: NoteMapping[];
|
||||
|
||||
// Load configuration from edit-docs-config.yaml
|
||||
async function loadConfig() {
|
||||
let CONFIG_PATH = configPath
|
||||
? path.resolve(configPath)
|
||||
: path.join(process.cwd(), "edit-docs-config.yaml");
|
||||
|
||||
const exists = await fs.access(CONFIG_PATH).then(() => true).catch(() => false);
|
||||
if (!exists && !configPath) {
|
||||
// Fallback to project root if running from within a subproject
|
||||
CONFIG_PATH = path.join(__dirname, "../../../edit-docs-config.yaml");
|
||||
}
|
||||
|
||||
const configContent = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||
const config = yaml.load(configContent) as Config;
|
||||
|
||||
BASE_URL = config.baseUrl;
|
||||
// Resolve all paths relative to the config file's directory (for flexibility with external configs)
|
||||
const CONFIG_DIR = path.dirname(CONFIG_PATH);
|
||||
NOTE_MAPPINGS = config.noteMappings.map((mapping) => ({
|
||||
...mapping,
|
||||
path: path.resolve(CONFIG_DIR, mapping.path)
|
||||
}));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await loadConfig();
|
||||
const initializedPromise = startElectron(() => {
|
||||
// Wait for the import to be finished and the application to be loaded before we listen to changes.
|
||||
setTimeout(() => registerHandlers(), 10_000);
|
||||
setTimeout(() => {
|
||||
registerHandlers();
|
||||
}, 10_000);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
for (const mapping of NOTE_MAPPINGS) {
|
||||
if (!mapping.exportOnly) {
|
||||
@@ -142,7 +211,7 @@ async function exportData(noteId: string, format: ExportFormat, outputPath: stri
|
||||
}
|
||||
}
|
||||
|
||||
const minifyMeta = (format === "html");
|
||||
const minifyMeta = (format === "html" || format === "share");
|
||||
await cleanUpMeta(outputPath, minifyMeta);
|
||||
}
|
||||
|
||||
@@ -195,7 +264,6 @@ async function registerHandlers() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Got entity changed", e.entityName, e.entity.title);
|
||||
debouncer();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
import cls from "@triliumnext/server/src/services/cls.js";
|
||||
import TaskContext from "@triliumnext/server/src/services/task_context.js";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import archiver, { type Archiver } from "archiver";
|
||||
import electron from "electron";
|
||||
import type { WriteStream } from "fs";
|
||||
import fs from "fs/promises";
|
||||
import fsExtra from "fs-extra";
|
||||
import path from "path";
|
||||
import electron from "electron";
|
||||
import windowService from "@triliumnext/server/src/services/window.js";
|
||||
import archiver, { type Archiver } from "archiver";
|
||||
import type { WriteStream } from "fs";
|
||||
import TaskContext from "@triliumnext/server/src/services/task_context.js";
|
||||
import { resolve } from "path";
|
||||
import { deferred, DeferredPromise } from "../../../packages/commons/src";
|
||||
|
||||
export function initializeDatabase(skipDemoDb: boolean) {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
const sqlInit = (await import("@triliumnext/server/src/services/sql_init.js")).default;
|
||||
cls.init(async () => {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
await sqlInit.createInitialDatabase(skipDemoDb);
|
||||
}
|
||||
resolve();
|
||||
import { deferred, type DeferredPromise } from "../../../packages/commons/src/index.js";
|
||||
|
||||
export function initializeDatabase(skipDemoDb: boolean): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
import("@triliumnext/server/src/services/sql_init.js").then((m) => {
|
||||
const sqlInit = m.default;
|
||||
cls.init(async () => {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
sqlInit.createInitialDatabase(skipDemoDb).then(() => resolve());
|
||||
} else {
|
||||
sqlInit.dbReady.resolve();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -32,10 +36,9 @@ export function initializeDatabase(skipDemoDb: boolean) {
|
||||
*/
|
||||
export function startElectron(callback: () => void): DeferredPromise<void> {
|
||||
const initializedPromise = deferred<void>();
|
||||
electron.app.on("ready", async () => {
|
||||
await initializedPromise;
|
||||
|
||||
console.log("Electron is ready!");
|
||||
const readyHandler = async () => {
|
||||
await initializedPromise;
|
||||
|
||||
// Start the server.
|
||||
const startTriliumServer = (await import("@triliumnext/server/src/www.js")).default;
|
||||
@@ -45,7 +48,15 @@ export function startElectron(callback: () => void): DeferredPromise<void> {
|
||||
await windowService.createMainWindow(electron.app);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
// Handle race condition: Electron ready event may have already fired
|
||||
if (electron.app.isReady()) {
|
||||
readyHandler();
|
||||
} else {
|
||||
electron.app.on("ready", readyHandler);
|
||||
}
|
||||
|
||||
return initializedPromise;
|
||||
}
|
||||
|
||||
@@ -70,7 +81,6 @@ async function createImportZip(path: string) {
|
||||
zlib: { level: 0 }
|
||||
});
|
||||
|
||||
console.log("Archive path is ", resolve(path))
|
||||
archive.directory(path, "/");
|
||||
|
||||
const outputStream = fsExtra.createWriteStream(inputFile);
|
||||
@@ -85,14 +95,16 @@ async function createImportZip(path: string) {
|
||||
}
|
||||
|
||||
function waitForEnd(archive: Archiver, stream: WriteStream) {
|
||||
return new Promise<void>(async (res, rej) => {
|
||||
stream.on("finish", () => res());
|
||||
await archive.finalize();
|
||||
return new Promise<void>((res, rej) => {
|
||||
stream.on("finish", res);
|
||||
stream.on("error", rej);
|
||||
archive.on("error", rej);
|
||||
archive.finalize().catch(rej);
|
||||
});
|
||||
}
|
||||
|
||||
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
|
||||
const promise = deferred<void>()
|
||||
const promise = deferred<void>();
|
||||
setTimeout(async () => {
|
||||
// Then extract the zip.
|
||||
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
|
||||
|
||||
17
apps/edit-docs/vitest.build.config.mts
Normal file
@@ -0,0 +1,17 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/edit-docs',
|
||||
plugins: [],
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: "node",
|
||||
include: ['spec/build-checks/**'],
|
||||
reporters: [
|
||||
"verbose"
|
||||
]
|
||||
},
|
||||
}));
|
||||
@@ -9,36 +9,36 @@ const baseURL = process.env['BASE_URL'] || `http://127.0.0.1:${port}`;
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: "src",
|
||||
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
|
||||
outputDir: "test-output",
|
||||
retries: 3,
|
||||
testDir: "src",
|
||||
reporter: [["list"], ["html", { outputFolder: "test-output" }]],
|
||||
outputDir: "test-output",
|
||||
retries: 3,
|
||||
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: !process.env.TRILIUM_DOCKER ? {
|
||||
command: 'pnpm start-prod-no-dir',
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
cwd: join(__dirname, "../server"),
|
||||
env: {
|
||||
TRILIUM_DATA_DIR: "spec/db",
|
||||
TRILIUM_PORT: port,
|
||||
TRILIUM_INTEGRATION_TEST: "memory"
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
timeout: 5 * 60 * 1000
|
||||
} : undefined,
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
}
|
||||
]
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: !process.env.TRILIUM_DOCKER ? {
|
||||
command: 'pnpm start-prod-no-dir',
|
||||
url: baseURL,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
cwd: join(__dirname, "../server"),
|
||||
env: {
|
||||
TRILIUM_DATA_DIR: "spec/db",
|
||||
TRILIUM_PORT: port,
|
||||
TRILIUM_INTEGRATION_TEST: "memory"
|
||||
},
|
||||
timeout: 5 * 60 * 1000
|
||||
} : undefined,
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { expect,test } from "@playwright/test";
|
||||
|
||||
import App from "../support/app";
|
||||
|
||||
const NOTE_TITLE = "Trilium Integration Test DB";
|
||||
@@ -65,21 +66,21 @@ test("Tabs are restored in right order", async ({ page, context }) => {
|
||||
// Open three tabs.
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Code notes");
|
||||
await expect(app.getActiveTab()).toContainText("Code notes");
|
||||
await app.addNewTab();
|
||||
await app.goToNoteInNewTab("Text notes");
|
||||
await expect(app.getActiveTab()).toContainText("Text notes");
|
||||
await app.addNewTab();
|
||||
await app.goToNoteInNewTab("Mermaid");
|
||||
await expect(app.getActiveTab()).toContainText("Mermaid");
|
||||
|
||||
// Select the mid one.
|
||||
await app.getTab(1).click();
|
||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||
await expect(app.getTab(0)).toContainText("Code notes");
|
||||
await expect(app.getTab(1)).toContainText("Text notes");
|
||||
await expect(app.getTab(2)).toContainText("Mermaid");
|
||||
|
||||
// Refresh the page and check the order.
|
||||
await app.goto( { preserveTabs: true });
|
||||
await expect(app.getTab(0)).toContainText("Code notes", { timeout: 15_000 });
|
||||
await expect(app.getTab(0)).toContainText("Code notes");
|
||||
await expect(app.getTab(1)).toContainText("Text notes");
|
||||
await expect(app.getTab(2)).toContainText("Mermaid");
|
||||
|
||||
@@ -128,8 +129,8 @@ test("New tab displays workspaces", async ({ page, context }) => {
|
||||
|
||||
const workspaceNotesEl = app.currentNoteSplitContent.locator(".workspace-notes");
|
||||
await expect(workspaceNotesEl).toBeVisible();
|
||||
expect(workspaceNotesEl).toContainText("Personal");
|
||||
expect(workspaceNotesEl).toContainText("Work");
|
||||
await expect(workspaceNotesEl).toContainText("Personal");
|
||||
await expect(workspaceNotesEl).toContainText("Work");
|
||||
await expect(workspaceNotesEl.locator(".bx.bxs-user")).toBeVisible();
|
||||
await expect(workspaceNotesEl.locator(".bx.bx-briefcase-alt")).toBeVisible();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import test, { BrowserContext, expect, Page } from "@playwright/test";
|
||||
import test, { expect, Page } from "@playwright/test";
|
||||
|
||||
import App from "../support/app";
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
const app = await setLayout({ page, context }, true);
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.setOption("rightPaneCollapsedItems", "[]");
|
||||
});
|
||||
test.afterEach(async ({ page, context }) => await setLayout({ page, context }, false));
|
||||
|
||||
test("Table of contents works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
@@ -73,13 +73,15 @@ test("Attachments listing works", async ({ page, context }) => {
|
||||
test("Download original PDF works", async ({ page, context }) => {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.goToNoteInNewTab("Dacia Logan.pdf");
|
||||
await app.goToNoteInNewTab("Layers test.pdf");
|
||||
const pdfHelper = new PdfHelper(app);
|
||||
await pdfHelper.toBeInitialized();
|
||||
|
||||
const downloadButton = app.currentNoteSplit.locator(".icon-action.bx.bx-download");
|
||||
await expect(downloadButton).toBeVisible();
|
||||
const [ download ] = await Promise.all([
|
||||
page.waitForEvent("download"),
|
||||
app.currentNoteSplit.locator(".icon-action.bx.bx-download").click()
|
||||
downloadButton.click()
|
||||
]);
|
||||
expect(download).toBeDefined();
|
||||
});
|
||||
@@ -105,13 +107,6 @@ test("Layers listing works", async ({ page, context }) => {
|
||||
await expect(layersList.locator(".pdf-layer-item")).toHaveCount(0);
|
||||
});
|
||||
|
||||
async function setLayout({ page, context}: { page: Page; context: BrowserContext }, newLayout: boolean) {
|
||||
const app = new App(page, context);
|
||||
await app.goto();
|
||||
await app.setOption("newLayout", newLayout ? "true" : "false");
|
||||
return app;
|
||||
}
|
||||
|
||||
class PdfHelper {
|
||||
private contentFrame: ReturnType<Page["frameLocator"]>;
|
||||
|
||||
@@ -125,5 +120,6 @@ class PdfHelper {
|
||||
|
||||
async toBeInitialized() {
|
||||
await expect(this.contentFrame.locator("#pageNumber")).toBeVisible();
|
||||
await expect(this.contentFrame.locator(".page")).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import App from "../support/app";
|
||||
|
||||
test("Table of contents is displayed", async ({ page, context }) => {
|
||||
@@ -8,7 +9,7 @@ test("Table of contents is displayed", async ({ page, context }) => {
|
||||
await app.goToNoteInNewTab("Table of contents");
|
||||
|
||||
await expect(app.sidebar).toContainText("Table of Contents");
|
||||
const rootList = app.sidebar.locator(".toc-widget > span > ol");
|
||||
const rootList = app.sidebar.locator(".toc > ol");
|
||||
|
||||
// Heading 1.1
|
||||
// Heading 1.1
|
||||
@@ -42,7 +43,7 @@ test("Highlights list is displayed", async ({ page, context }) => {
|
||||
await app.closeAllTabs();
|
||||
await app.goToNoteInNewTab("Highlights list");
|
||||
|
||||
await expect(app.sidebar).toContainText("Highlights List");
|
||||
await expect(app.sidebar).toContainText(/highlights/i);
|
||||
const rootList = app.sidebar.locator(".highlights-list ol");
|
||||
let index = 0;
|
||||
for (const highlightedEl of ["Bold 1", "Italic 1", "Underline 1", "Colored text 1", "Background text 1", "Bold 2", "Italic 2", "Underline 2", "Colored text 2", "Background text 2"]) {
|
||||
@@ -63,7 +64,9 @@ test("Displays math popup", async ({ page, context }) => {
|
||||
const mathForm = page.locator(".ck-math-form");
|
||||
await expect(mathForm).toBeVisible();
|
||||
|
||||
const input = mathForm.locator(".ck-input").first();
|
||||
const input = mathForm.locator(".ck-latex-textarea").first();
|
||||
await expect(input).toBeVisible();
|
||||
await expect(input).toBeEnabled();
|
||||
await input.click();
|
||||
await input.fill("e=mc^2");
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class App {
|
||||
this.noteTreeHoistedNote = this.noteTree.locator(".fancytree-node", { has: page.locator(".unhoist-button") });
|
||||
this.launcherBar = page.locator("#launcher-container");
|
||||
this.currentNoteSplit = page.locator(".note-split:not(.hidden-ext)");
|
||||
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title");
|
||||
this.currentNoteSplitTitle = this.currentNoteSplit.locator(".note-title").first();
|
||||
this.currentNoteSplitContent = this.currentNoteSplit.locator(".note-detail-printable.visible");
|
||||
this.sidebar = page.locator("#right-pane");
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export default class App {
|
||||
|
||||
// Wait for the page to load.
|
||||
if (url === "/") {
|
||||
await expect(this.page.locator(".tree")).toContainText("Trilium Integration Test");
|
||||
await expect(this.noteTree).toContainText("Trilium Integration Test");
|
||||
if (!preserveTabs) {
|
||||
await this.closeAllTabs();
|
||||
}
|
||||
@@ -68,13 +68,15 @@ export default class App {
|
||||
|
||||
async goToNoteInNewTab(noteTitle: string) {
|
||||
const autocomplete = this.currentNoteSplit.locator(".note-autocomplete");
|
||||
await expect(autocomplete).toBeVisible();
|
||||
await autocomplete.fill(noteTitle);
|
||||
|
||||
const resultsSelector = this.currentNoteSplit.locator(".note-detail-empty-results");
|
||||
await expect(resultsSelector).toContainText(noteTitle);
|
||||
await resultsSelector.locator(".aa-suggestion", { hasText: noteTitle })
|
||||
.nth(1) // Select the second one, as the first one is "Create a new note"
|
||||
.click();
|
||||
const suggestionSelector = resultsSelector.locator(".aa-suggestion")
|
||||
.nth(1); // Select the second one (best candidate), as the first one is "Create a new note"
|
||||
await expect(suggestionSelector).toContainText(noteTitle);
|
||||
suggestionSelector.click();
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"better-sqlite3": "12.5.0"
|
||||
"better-sqlite3": "12.6.0"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@triliumnext/server",
|
||||
"version": "0.101.1",
|
||||
"version": "0.101.3",
|
||||
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
|
||||
"private": true,
|
||||
"main": "./src/main.ts",
|
||||
@@ -29,16 +29,15 @@
|
||||
"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": {
|
||||
"better-sqlite3": "12.5.0",
|
||||
"better-sqlite3": "12.6.0",
|
||||
"html-to-text": "9.0.5",
|
||||
"node-html-parser": "7.0.1",
|
||||
"node-html-parser": "7.0.2",
|
||||
"sucrase": "3.35.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.71.2",
|
||||
"@braintree/sanitize-url": "7.1.1",
|
||||
"@electron/remote": "2.1.3",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@triliumnext/commons": "workspace:*",
|
||||
"@triliumnext/express-partial-content": "workspace:*",
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
@@ -90,7 +89,7 @@
|
||||
"escape-html": "1.0.3",
|
||||
"express": "5.2.1",
|
||||
"express-http-proxy": "2.1.2",
|
||||
"express-openid-connect": "2.19.3",
|
||||
"express-openid-connect": "2.19.4",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"express-session": "1.18.2",
|
||||
"file-uri-to-path": "2.0.0",
|
||||
@@ -100,7 +99,7 @@
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.6",
|
||||
"i18next": "25.7.3",
|
||||
"i18next": "25.7.4",
|
||||
"i18next-fs-backend": "2.6.1",
|
||||
"image-type": "6.0.0",
|
||||
"ini": "6.0.0",
|
||||
@@ -113,24 +112,24 @@
|
||||
"multer": "2.0.2",
|
||||
"normalize-strings": "1.1.1",
|
||||
"ollama": "0.6.3",
|
||||
"openai": "6.15.0",
|
||||
"openai": "6.16.0",
|
||||
"rand-token": "1.0.1",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sax": "1.4.3",
|
||||
"sax": "1.4.4",
|
||||
"serve-favicon": "2.5.1",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
"striptags": "3.2.0",
|
||||
"supertest": "7.1.4",
|
||||
"supertest": "7.2.2",
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"time2fa": "1.4.2",
|
||||
"tmp": "0.2.5",
|
||||
"turndown": "7.2.2",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "7.3.0",
|
||||
"ws": "8.18.3",
|
||||
"vite": "7.3.1",
|
||||
"ws": "8.19.0",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0"
|
||||
}
|
||||
|
||||
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/1_Hiding the subtree_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
95
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Hiding the subtree.html
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
<figure class="image image-style-align-right">
|
||||
<img style="aspect-ratio:328/45;" src="1_Hiding the subtree_image.png"
|
||||
width="328" height="45">
|
||||
<figcaption>An example of a collection with a relatively large number of children
|
||||
that are hidden from the tree.</figcaption>
|
||||
</figure>
|
||||
<p>The tree works well when the notes are structured in a hierarchy so that
|
||||
the number of items stays small. When a note has a large number of notes
|
||||
(in the order of thousands or tens of thousands), two problems arise:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e536c86d371061c12f76f7de2a0af67be">Navigating between notes becomes cumbersome and the tree itself gets cluttered
|
||||
with a large amount of notes.</li>
|
||||
<li data-list-item-id="ecc37d6c4d0430254e98615842b94429d">The large amount of notes can slow down the application considerably.</li>
|
||||
</ul>
|
||||
<p>Since v0.102.0, Trilium allows the tree to hide the child notes of particular
|
||||
notes. This works for both <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> and
|
||||
normal notes.</p>
|
||||
<h2>Interaction</h2>
|
||||
<p>When the subtree of a note is hidden, there are a few subtle changes:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="ec1ce3d2030f36e4847f3bbd9468d28e3">To indicate that the subtree is hidden, the note will not have an expand
|
||||
button and it will display the number of children to the right.</li>
|
||||
<li
|
||||
data-list-item-id="ea99d38ea6c8a816cf2ab7a7e73cfcac5">It's not possible to add a new note directly from the tree.
|
||||
<ul>
|
||||
<li data-list-item-id="ef0132a903a11e9f667b2b2f4c4fff17a">For <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a>,
|
||||
it's best to use the built-in mechanism to create notes (for example by
|
||||
creating a new point on a geo-map, or by adding a new row in a table).</li>
|
||||
<li
|
||||
data-list-item-id="e7db44100046c8c79bf79841285aacd1f">For normal notes, it's still possible to create children via other means
|
||||
such as using the <a class="reference-link" href="#root/pOsGYCXsbNQG/KSZ04uQ2D1St/iPIMuisry3hd/QEAPj01N5f7w/_help_hrZ1D00cLbal">Internal (reference) links</a> system.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb049f46cf91db6de113af1099a14944e">Notes can be dragged from outside the note, case in which they will be
|
||||
cloned into it.
|
||||
<ul>
|
||||
<li data-list-item-id="e96d9b7a0755e9c054bab5db4fc1aa25e">Instead of switching to the child notes that were copied, the parent note
|
||||
is highlighted instead.</li>
|
||||
<li data-list-item-id="ec667e3f94a0cfa3fa41ce38d3ed6ee95">A notification will indicate this behavior.</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li data-list-item-id="eb64670dd7ace6764c18602b440f88049">Similarly, features such as cut/copy and then paste into the note will
|
||||
also work.</li>
|
||||
</ul>
|
||||
<h2>Spotlighting</h2>
|
||||
<figure class="image image-style-align-right">
|
||||
<img style="aspect-ratio:322/83;" src="Hiding the subtree_image.png"
|
||||
width="322" height="83">
|
||||
</figure>
|
||||
<p>Even if the subtree of a note is hidden, if a child note manages to become
|
||||
active, it will still appear inside the tree in a special state called <em>spotlighted</em>.</p>
|
||||
<p>During this state, the note remains under its normal hierarchy, so that
|
||||
its easy to tell its location. In addition, this means that:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e2490369eb3d99ca694dba23a3410abef">The note position is clearly visible when using the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>.</li>
|
||||
<li
|
||||
data-list-item-id="e041d3807f80dc77b022540b0551b8376">The note can still be operated on from the tree, such as adding a
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/IakOLONlIfGI/_help_TBwsyfadTA18">Branch prefix</a> or moving it outside the collection.</li>
|
||||
</ul>
|
||||
<p>The note appears in italics to indicate its temporary display. When switching
|
||||
to another note, the spotlighted note will disappear.</p>
|
||||
<aside class="admonition note">
|
||||
<p>Only one note can be highlighted at the time. When working with multiple
|
||||
notes such as dragging them into the collection, no note will be spotlighted.
|
||||
This is intentional to avoid displaying a partial state of the subtree.</p>
|
||||
</aside>
|
||||
<h2>Working with collections</h2>
|
||||
<p>By default, some of the <a class="reference-link" href="#root/pOsGYCXsbNQG/_help_GTwFsgaA0lCt">Collections</a> will
|
||||
automatically hide their child notes, for example the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_CtBQqbwXDx1w">Kanban Board</a> or
|
||||
the <a class="reference-link" href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_2FvYrpmOXm29">Table</a>.</p>
|
||||
<p>The reasoning behind this is that collections are generally opaque to
|
||||
the rest of the notes and they can generate a large amount of sub-notes
|
||||
since they intentionally lack structure (in order to allow easy swapping
|
||||
between views).</p>
|
||||
<p>Some types of collections have the child notes intentionally shown, for
|
||||
example the legacy ones (Grid and List), but also the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/GTwFsgaA0lCt/_help_zP3PMqaG71Ct">Presentation</a> which
|
||||
requires the tree structure in order to organize and edit the slides.</p>
|
||||
<p>To toggle this behavior:</p>
|
||||
<ul>
|
||||
<li data-list-item-id="e6d8c8c98802d70f13df626ea1f062122">In the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>,
|
||||
press the Options button underneath the title and uncheck <em>Hide child notes in tree</em>.</li>
|
||||
<li
|
||||
data-list-item-id="e2398432e127c54239d679a6b13d8390b">Right click the collection note in the <a class="reference-link"
|
||||
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Show subtree</em>.</li>
|
||||
</ul>
|
||||
<h2>Working with normal notes</h2>
|
||||
<p>It's possible to hide the subtree for normal notes as well, not just collections.
|
||||
To do so, right click the note in the <a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a> and
|
||||
select <em>Advanced</em> → <em>Hide subtree.</em>
|
||||
</p>
|
||||
BIN
apps/server/src/assets/doc_notes/en/User Guide/User Guide/Basic Concepts and Features/UI Elements/Note Tree/Hiding the subtree_image.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
2
apps/server/src/assets/doc_notes/en/User Guide/User Guide/FAQ.html
generated
vendored
@@ -47,7 +47,7 @@
|
||||
href="#root/_help_tAassRL4RSQL">data directory</a>in the <code spellcheck="false">TRILIUM_DATA_DIR</code> environment
|
||||
variable and separate port on <code spellcheck="false">TRILIUM_PORT</code> environment
|
||||
variable. How to do that depends on the platform, in Unix-based systems
|
||||
you can achieve that by running command such as this:</p><pre><code class="language-text-x-trilium-auto">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
|
||||
you can achieve that by running command such as this:</p><pre><code class="language-text-x-sh">TRILIUM_DATA_DIR=/home/me/path/to/data/dir TRILIUM_PORT=12345 trilium </code></pre>
|
||||
<p>You can save this command into a <code spellcheck="false">.sh</code> script
|
||||
file or make an alias. Do this similarly for a second instance with different
|
||||
data directory and port.</p>
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"password-confirmation": "密码确认",
|
||||
"button": "设置密码"
|
||||
},
|
||||
"javascript-required": "Trilium需要启用JavaScript。",
|
||||
"setup": {
|
||||
"heading": "TriliumNext笔记设置",
|
||||
"new-document": "我是新用户,我想为我的笔记创建一个新的Trilium文档",
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"password-confirmation": "Passwortbestätigung",
|
||||
"button": "Passwort festlegen"
|
||||
},
|
||||
"javascript-required": "Trilium erfordert, dass JavaScript aktiviert ist.",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes Setup",
|
||||
"new-document": "Ich bin ein neuer Benutzer und möchte ein neues Trilium-Dokument für meine Notizen erstellen",
|
||||
|
||||
@@ -220,7 +220,6 @@
|
||||
"password-confirmation": "Password confirmation",
|
||||
"button": "Set password"
|
||||
},
|
||||
"javascript-required": "Trilium requires JavaScript to be enabled.",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes setup",
|
||||
"new-document": "I'm a new user, and I want to create a new Trilium document for my notes",
|
||||
@@ -382,6 +381,8 @@
|
||||
"tooltip": "Trilium Notes",
|
||||
"close": "Quit Trilium",
|
||||
"recents": "Recent notes",
|
||||
"recently-closed-windows": "Recently closed windows",
|
||||
"tabs-total": "{{number}} tabs total",
|
||||
"bookmarks": "Bookmarks",
|
||||
"today": "Open today's journal note",
|
||||
"new-note": "New note",
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"password-confirmation": "Confirmación de contraseña",
|
||||
"button": "Establecer contraseña"
|
||||
},
|
||||
"javascript-required": "Trilium requiere que JavaScript esté habilitado.",
|
||||
"setup": {
|
||||
"heading": "Configuración de Trilium Notes",
|
||||
"new-document": "Soy un usuario nuevo y quiero crear un nuevo documento de Trilium para mis notas",
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
"password-confirmation": "Confirmation du mot de passe",
|
||||
"button": "Définir le mot de passe"
|
||||
},
|
||||
"javascript-required": "Trilium nécessite que JavaScript soit activé.",
|
||||
"setup": {
|
||||
"heading": "Configuration de Trilium Notes",
|
||||
"new-document": "Je suis un nouvel utilisateur et je souhaite créer un nouveau document Trilium pour mes notes",
|
||||
|
||||
@@ -10,6 +10,23 @@
|
||||
"creating-and-moving-notes": "नोट्स बनाना और स्थानांतरित करना",
|
||||
"move-note-up": "नोट को ऊपर ले जाएं",
|
||||
"move-note-down": "नोट को नीचे ले जाएं",
|
||||
"note-clipboard": "नोट क्लिपबोर्ड"
|
||||
"note-clipboard": "नोट क्लिपबोर्ड",
|
||||
"duplicate-subtree": "डुप्लिकेट सबट्री",
|
||||
"open-new-tab": "नया टैब खोलें",
|
||||
"second-tab": "लिस्ट में दूसरी टैब एक्टिवेट करें",
|
||||
"third-tab": "लिस्ट में तीसरी टैब एक्टिवेट करें",
|
||||
"fourth-tab": "लिस्ट में चौथी टैब एक्टिवेट करें",
|
||||
"sixth-tab": "लिस्ट में छठी टैब एक्टिवेट करें",
|
||||
"seventh-tab": "लिस्ट में सातवीं टैब एक्टिवेट करें",
|
||||
"eight-tab": "लिस्ट में आठवीं टैब एक्टिवेट करें",
|
||||
"ninth-tab": "लिस्ट में नौवीं टैब एक्टिवेट करें",
|
||||
"last-tab": "लिस्ट में आखिरी टैब एक्टिवेट करें",
|
||||
"show-sql-console": "\"SQL कंसोल\" पेज खोलें",
|
||||
"show-backend-log": "\"बैकेंड लॉग\" पेज खोलें",
|
||||
"quick-search": "क्विक सर्च बार को एक्टिवेट करें",
|
||||
"search-in-subtree": "एक्टिव नोट के सब-ट्री में नोट्स खोजें",
|
||||
"expand-subtree": "मौजूदा नोट के सब-ट्री को (subtree) एक्सपैंड करें",
|
||||
"delete-note": "नोट डिलीट करें",
|
||||
"move-note-up-in-hierarchy": "नोट एक लेवल ऊपर मूव करें"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,6 @@
|
||||
"password-confirmation": "Conferma della password",
|
||||
"button": "Imposta password"
|
||||
},
|
||||
"javascript-required": "Trilium richiede JavaScript abilitato per funzionare.",
|
||||
"setup": {
|
||||
"heading": "Configurazione di Trilium Notes",
|
||||
"new-document": "Sono un nuovo utente, e desidero creare un nuovo documento Trilium per le mie note",
|
||||
|
||||
@@ -220,7 +220,6 @@
|
||||
"button": "パスワードの設定",
|
||||
"password-confirmation": "パスワードの再入力"
|
||||
},
|
||||
"javascript-required": "Triliumを使用するにはJavaScriptを有効にする必要があります。",
|
||||
"setup": {
|
||||
"heading": "Trilium Notes セットアップ",
|
||||
"new-document": "私は新しいユーザーで、ノートを取るために新しいTriliumドキュメントを作成したい",
|
||||
|
||||
@@ -7,6 +7,37 @@
|
||||
"scroll-to-active-note": "Skroll notat-treet til aktivt notat",
|
||||
"quick-search": "Aktiver hurtigsøk-feltet",
|
||||
"search-in-subtree": "Søk etter notater i det aktive notatets understruktur",
|
||||
"creating-and-moving-notes": "Lage og flytte notater"
|
||||
"creating-and-moving-notes": "Lage og flytte notater",
|
||||
"dialogs": "Dialogbokser",
|
||||
"other": "Andre"
|
||||
},
|
||||
"setup_sync-from-desktop": {
|
||||
"step6-here": "her"
|
||||
},
|
||||
"set_password": {
|
||||
"password": "Passord"
|
||||
},
|
||||
"setup": {
|
||||
"next": "Neste",
|
||||
"title": "Konfigurasjon"
|
||||
},
|
||||
"login": {
|
||||
"title": "Logg inn",
|
||||
"password": "Passord",
|
||||
"button": "Logg inn"
|
||||
},
|
||||
"setup_sync-from-server": {
|
||||
"server-host-placeholder": "https://<hostnavn>:<port>",
|
||||
"proxy-server-placeholder": "https://<hostnavn>:<port>",
|
||||
"note": "Obs:",
|
||||
"password": "Passord",
|
||||
"password-placeholder": "Passord",
|
||||
"back": "Tilbake"
|
||||
},
|
||||
"setup_sync-in-progress": {
|
||||
"outstanding-items-default": "N/A"
|
||||
},
|
||||
"share_page": {
|
||||
"parent": "overordnet notat:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,6 @@
|
||||
"button": "Zaloguj",
|
||||
"sign_in_with_sso": "Zaloguj przez {{ ssoIssuerName }}"
|
||||
},
|
||||
"javascript-required": "Trilium wymaga włączenia obsługi JavaScript.",
|
||||
"setup_sync-from-server": {
|
||||
"server-host": "Adres serwera Trilium",
|
||||
"proxy-server": "Serwer proxy (opcjonalnie)",
|
||||
|
||||