mirror of
https://github.com/zadam/trilium.git
synced 2025-11-12 08:15:52 +01:00
Compare commits
287 Commits
feat/rice-
...
v0.99.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25563c6687 | ||
|
|
48608adbd3 | ||
|
|
362ecba98d | ||
|
|
a13892da66 | ||
|
|
e8dc19a1a6 | ||
|
|
cc326547d1 | ||
|
|
a45b147462 | ||
|
|
4e73f20165 | ||
|
|
80c77eeb18 | ||
|
|
c30282fbd0 | ||
|
|
7b8f1ed6ec | ||
|
|
118e11c3fd | ||
|
|
b54765113e | ||
|
|
ed08893996 | ||
|
|
20286d53c8 | ||
|
|
9c1a34fe7c | ||
|
|
e70c6b69b8 | ||
|
|
9b69b0ad0d | ||
|
|
3776c40b8d | ||
|
|
c7369bc9b3 | ||
|
|
b741662fde | ||
|
|
624610b17c | ||
|
|
de004bd8ba | ||
|
|
06eb30c69d | ||
|
|
b9b5c13d9c | ||
|
|
8e697d0578 | ||
|
|
6f245ec8d5 | ||
|
|
532df6559a | ||
|
|
8589f7f164 | ||
|
|
4eadf40e20 | ||
|
|
7b9303b392 | ||
|
|
521152ec0e | ||
|
|
2ff746253d | ||
|
|
c270aef738 | ||
|
|
d0881c09ed | ||
|
|
b905c1d03a | ||
|
|
f03448bae4 | ||
|
|
f8ac09df38 | ||
|
|
15c329c331 | ||
|
|
f6afc0b718 | ||
|
|
ecfa333491 | ||
|
|
21b0ef9554 | ||
|
|
2f3be96dff | ||
|
|
4ba7907bee | ||
|
|
afa92551ea | ||
|
|
cc0646e79c | ||
|
|
4c8f20be9a | ||
|
|
3407528c03 | ||
|
|
70575a00cb | ||
|
|
74ba4b9ee5 | ||
|
|
706abeb307 | ||
|
|
7158c48831 | ||
|
|
78e2814068 | ||
|
|
7659224e3a | ||
|
|
baff349fa2 | ||
|
|
50869d29db | ||
|
|
c4603fce25 | ||
|
|
220aab2b76 | ||
|
|
285a7253e3 | ||
|
|
d8d80ed936 | ||
|
|
3463cb83a0 | ||
|
|
b0bd60b9a4 | ||
|
|
3d70a0534b | ||
|
|
53805e9c49 | ||
|
|
0e95610d4e | ||
|
|
051e2b4eef | ||
|
|
1d750bde64 | ||
|
|
4476615d33 | ||
|
|
bbcc16daab | ||
|
|
457dd070c6 | ||
|
|
ce229dd6f5 | ||
|
|
2e59d9d7bc | ||
|
|
bbe96c3967 | ||
|
|
d95eb9f5d3 | ||
|
|
d75279316a | ||
|
|
7f6be13a18 | ||
|
|
2f74b40095 | ||
|
|
a844e1faab | ||
|
|
f629f564cd | ||
|
|
9a5f2f8d3b | ||
|
|
9bccc72668 | ||
|
|
a29597a4bf | ||
|
|
44b34d1ea0 | ||
|
|
c617c84d86 | ||
|
|
b38780755a | ||
|
|
78a54fa9f7 | ||
|
|
fa64ca2c93 | ||
|
|
71d7403690 | ||
|
|
e4cd946ea8 | ||
|
|
cedd1c4789 | ||
|
|
7a677cff5f | ||
|
|
2d03dd22e3 | ||
|
|
e28da416ba | ||
|
|
28c0ef52f3 | ||
|
|
9464a64d64 | ||
|
|
c3af6a6aa2 | ||
|
|
a4281fe26f | ||
|
|
5c0d6e1fef | ||
|
|
23f1103822 | ||
|
|
b45ee6879c | ||
|
|
a8116aa264 | ||
|
|
db3960a23e | ||
|
|
a43e08500e | ||
|
|
9011d648b5 | ||
|
|
30c1708979 | ||
|
|
1a55d3433d | ||
|
|
bec47c0bb2 | ||
|
|
4fdb502a19 | ||
|
|
e9ccd52fd5 | ||
|
|
dcd30972bd | ||
|
|
4578541fa8 | ||
|
|
914fa3625f | ||
|
|
5e3ffc12ce | ||
|
|
aa3a8d19ae | ||
|
|
a57c237c69 | ||
|
|
b52e615f0c | ||
|
|
95e5c2563e | ||
|
|
33be7f828b | ||
|
|
d23d37baac | ||
|
|
dda8b2795b | ||
|
|
0b9eb6c532 | ||
|
|
728f574eac | ||
|
|
8e90826aef | ||
|
|
5e5e0afcf0 | ||
|
|
de85d1f0df | ||
|
|
b287b892e1 | ||
|
|
a0edf00caa | ||
|
|
218b9404fc | ||
|
|
54af120e96 | ||
|
|
ba61ab18ff | ||
|
|
2e14522f86 | ||
|
|
4ae38ac5d6 | ||
|
|
d5cb6a86c8 | ||
|
|
a577fd45e2 | ||
|
|
977284fe57 | ||
|
|
26ea43d604 | ||
|
|
4cb328bdb3 | ||
|
|
16785a5c0b | ||
|
|
d271fe7fdd | ||
|
|
88b9709f15 | ||
|
|
f55edabe92 | ||
|
|
2b983f871e | ||
|
|
ab298cbb3b | ||
|
|
abeeea584f | ||
|
|
4d5597cc75 | ||
|
|
c684712141 | ||
|
|
a8bb301296 | ||
|
|
d5bfa466a2 | ||
|
|
7651c58c47 | ||
|
|
c2ce36d963 | ||
|
|
3359ff5470 | ||
|
|
421c1f257e | ||
|
|
97e87741ee | ||
|
|
193caf8c21 | ||
|
|
d521bda6ab | ||
|
|
b80cb22985 | ||
|
|
7131d44d03 | ||
|
|
7369f9d532 | ||
|
|
58ac325634 | ||
|
|
579b2ce76e | ||
|
|
0494032fb5 | ||
|
|
48853555f5 | ||
|
|
0cd6f0d267 | ||
|
|
0ae4defc6d | ||
|
|
db644f20ed | ||
|
|
59a2ef7527 | ||
|
|
757a046474 | ||
|
|
aeb0f44a43 | ||
|
|
5186ea3fff | ||
|
|
70a4feff50 | ||
|
|
91f85e6675 | ||
|
|
0cb989e74f | ||
|
|
d4e31e9d98 | ||
|
|
6c3b5314c8 | ||
|
|
3faac9f26e | ||
|
|
c31ac1a6ee | ||
|
|
9eff6ad4c2 | ||
|
|
9deb7ba4e9 | ||
|
|
93d77ca06e | ||
|
|
a42daccc2e | ||
|
|
33c64b604e | ||
|
|
f89c14b35a | ||
|
|
1fa3420abe | ||
|
|
f8b8edd5aa | ||
|
|
4c90319c9e | ||
|
|
3b531544a3 | ||
|
|
25d9695db0 | ||
|
|
caf88473f6 | ||
|
|
b32dc18cf6 | ||
|
|
5a7349121a | ||
|
|
dcc5b9f422 | ||
|
|
0b01890a11 | ||
|
|
686c8936cb | ||
|
|
d9071f2d8e | ||
|
|
2a0472ae42 | ||
|
|
8c2354df71 | ||
|
|
d650b801e6 | ||
|
|
fbb27b512e | ||
|
|
3ae7bd59ec | ||
|
|
55f7a26634 | ||
|
|
aa7f01313a | ||
|
|
6f82c283e9 | ||
|
|
aa7ecaf684 | ||
|
|
50a69248a7 | ||
|
|
5782e58db1 | ||
|
|
e8bf12c4ab | ||
|
|
344f2d819e | ||
|
|
d2e9101675 | ||
|
|
82e5de2261 | ||
|
|
ff4cd7eae5 | ||
|
|
a62c9a1a2f | ||
|
|
cf406383c3 | ||
|
|
07fe42d04e | ||
|
|
5b8bb8587d | ||
|
|
154492e454 | ||
|
|
b1729ad7ec | ||
|
|
daec11b981 | ||
|
|
7cdd8ffbe2 | ||
|
|
25ac9e2aa1 | ||
|
|
4fc434a52e | ||
|
|
3e0d1bfa44 | ||
|
|
3b02eb8851 | ||
|
|
4c5b2a7c75 | ||
|
|
e6d2009605 | ||
|
|
5d706a88d8 | ||
|
|
c7c7e05106 | ||
|
|
e250107202 | ||
|
|
c141cbcf07 | ||
|
|
8e83562e6a | ||
|
|
08197f56d0 | ||
|
|
ff0d8a70ad | ||
|
|
605b317f29 | ||
|
|
6f0a264869 | ||
|
|
6a89e096e5 | ||
|
|
e4e0f7619b | ||
|
|
f3f07cdd28 | ||
|
|
ef82c3d48b | ||
|
|
214ba5265a | ||
|
|
ecb6dc7923 | ||
|
|
131fb43ab7 | ||
|
|
8a76fdb8d1 | ||
|
|
8e3dbb2f65 | ||
|
|
8b4fee1680 | ||
|
|
a370b52614 | ||
|
|
4063229982 | ||
|
|
1ef03b7a77 | ||
|
|
e510653edb | ||
|
|
a2c523def1 | ||
|
|
e3604edad7 | ||
|
|
426d8296be | ||
|
|
947e43a615 | ||
|
|
0424fe4fba | ||
|
|
f789b69506 | ||
|
|
5df512a69c | ||
|
|
2a5f329ada | ||
|
|
4fe3944585 | ||
|
|
98b8e97fd9 | ||
|
|
38a1cd0d35 | ||
|
|
ae544a80c2 | ||
|
|
8d88411fda | ||
|
|
ea45024559 | ||
|
|
64d3589b40 | ||
|
|
638cb4281e | ||
|
|
1568908982 | ||
|
|
4459561308 | ||
|
|
3341e59a80 | ||
|
|
74a805056b | ||
|
|
f42e870de1 | ||
|
|
ca3964f8b7 | ||
|
|
ddafda5f4e | ||
|
|
40b08e1828 | ||
|
|
5141f0a0d5 | ||
|
|
8c165c0401 | ||
|
|
b4dd40e128 | ||
|
|
535b960b76 | ||
|
|
b58f37cd4a | ||
|
|
a01fb39599 | ||
|
|
be15934b22 | ||
|
|
96b3464f00 | ||
|
|
2470b0b334 | ||
|
|
4344687303 | ||
|
|
b4fe46eba3 | ||
|
|
8e227a6146 | ||
|
|
b03cb1ce1b | ||
|
|
fb0d971e48 | ||
|
|
4fa4112840 | ||
|
|
50f0b88eff |
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -77,7 +77,7 @@ jobs:
|
|||||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGN_KEY }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.1
|
uses: softprops/action-gh-release@v2.4.2
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
@@ -118,7 +118,7 @@ jobs:
|
|||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: softprops/action-gh-release@v2.4.1
|
uses: softprops/action-gh-release@v2.4.2
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
if: ${{ github.event_name != 'pull_request' }}
|
||||||
with:
|
with:
|
||||||
make_latest: false
|
make_latest: false
|
||||||
|
|||||||
54
.github/workflows/playwright.yml
vendored
54
.github/workflows/playwright.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- hotfix
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "apps/website/**"
|
- "apps/website/**"
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -13,8 +14,24 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
e2e:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: linux-x64
|
||||||
|
os: ubuntu-22.04
|
||||||
|
arch: x64
|
||||||
|
- name: linux-arm64
|
||||||
|
os: ubuntu-24.04-arm
|
||||||
|
arch: arm64
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
name: E2E tests on ${{ matrix.name }}
|
||||||
|
env:
|
||||||
|
TRILIUM_DOCKER: 1
|
||||||
|
TRILIUM_PORT: 8082
|
||||||
|
TRILIUM_DATA_DIR: "${{ github.workspace }}/apps/server/spec/db"
|
||||||
|
TRILIUM_INTEGRATION_TEST: memory
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
@@ -29,9 +46,34 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm exec playwright install --with-deps
|
|
||||||
|
|
||||||
- run: pnpm --filter server-e2e e2e
|
- name: Install Playwright browsers
|
||||||
|
run: pnpm exec playwright install --with-deps
|
||||||
|
|
||||||
|
- name: Build the server
|
||||||
|
uses: ./.github/actions/build-server
|
||||||
|
with:
|
||||||
|
os: linux
|
||||||
|
arch: ${{ matrix.arch }}
|
||||||
|
|
||||||
|
- name: Unpack and start the server
|
||||||
|
run: |
|
||||||
|
version=$(node --eval "console.log(require('./package.json').version)")
|
||||||
|
file=$(find ./upload -name '*.tar.xz' -print -quit)
|
||||||
|
name=$(basename "$file" .tar.xz)
|
||||||
|
mkdir -p ./server-dist
|
||||||
|
tar -xvf "$file" -C ./server-dist
|
||||||
|
server_dir="./server-dist/TriliumNotes-Server-$version-linux-${{ matrix.arch }}"
|
||||||
|
if [ ! -d "$server_dir" ]; then
|
||||||
|
echo Missing dir.
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cd "$server_dir"
|
||||||
|
"./trilium.sh" &
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
- name: Server end-to-end tests
|
||||||
|
run: pnpm --filter server-e2e e2e
|
||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: failure()
|
if: failure()
|
||||||
@@ -39,3 +81,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: e2e report
|
name: e2e report
|
||||||
path: apps/server-e2e/test-output
|
path: apps/server-e2e/test-output
|
||||||
|
|
||||||
|
- name: Kill the server
|
||||||
|
if: always()
|
||||||
|
run: pkill -f trilium || true
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -127,7 +127,7 @@ jobs:
|
|||||||
path: upload
|
path: upload
|
||||||
|
|
||||||
- name: Publish stable release
|
- name: Publish stable release
|
||||||
uses: softprops/action-gh-release@v2.4.1
|
uses: softprops/action-gh-release@v2.4.2
|
||||||
with:
|
with:
|
||||||
draft: false
|
draft: false
|
||||||
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
body_path: docs/Release Notes/Release Notes/${{ github.ref_name }}.md
|
||||||
|
|||||||
@@ -41,12 +41,12 @@
|
|||||||
"@types/node": "24.10.0",
|
"@types/node": "24.10.0",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.39.0",
|
"eslint": "9.39.1",
|
||||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
"jsdoc": "4.0.5",
|
"jsdoc": "4.0.5",
|
||||||
"lorem-ipsum": "2.0.8",
|
"lorem-ipsum": "2.0.8",
|
||||||
"rcedit": "4.0.1",
|
"rcedit": "5.0.0",
|
||||||
"rimraf": "6.1.0",
|
"rimraf": "6.1.0",
|
||||||
"tslib": "2.8.1"
|
"tslib": "2.8.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"packageManager": "pnpm@10.20.0",
|
"packageManager": "pnpm@10.20.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redocly/cli": "2.10.0",
|
"@redocly/cli": "2.11.0",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"fs-extra": "11.3.2",
|
"fs-extra": "11.3.2",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ async function main() {
|
|||||||
buildSwagger(context);
|
buildSwagger(context);
|
||||||
buildScriptApi(context);
|
buildScriptApi(context);
|
||||||
|
|
||||||
// Copy index file.
|
// Copy index and 404 files.
|
||||||
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
|
cpSync(join(__dirname, "index.html"), join(context.baseDir, "index.html"));
|
||||||
|
cpSync(join(context.baseDir, "user-guide/404.html"), join(context.baseDir, "404.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/client",
|
"name": "@triliumnext/client",
|
||||||
"version": "0.99.3",
|
"version": "0.99.5",
|
||||||
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
"circular-deps": "dpdm -T src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "9.39.0",
|
"@eslint/js": "9.39.1",
|
||||||
"@excalidraw/excalidraw": "0.18.0",
|
"@excalidraw/excalidraw": "0.18.0",
|
||||||
"@fullcalendar/core": "6.1.19",
|
"@fullcalendar/core": "6.1.19",
|
||||||
"@fullcalendar/daygrid": "6.1.19",
|
"@fullcalendar/daygrid": "6.1.19",
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
"draggabilly": "3.0.0",
|
"draggabilly": "3.0.0",
|
||||||
"force-graph": "1.51.0",
|
"force-graph": "1.51.0",
|
||||||
"globals": "16.5.0",
|
"globals": "16.5.0",
|
||||||
"i18next": "25.6.0",
|
"i18next": "25.6.1",
|
||||||
"i18next-http-backend": "3.0.2",
|
"i18next-http-backend": "3.0.2",
|
||||||
"jquery": "3.7.1",
|
"jquery": "3.7.1",
|
||||||
"jquery.fancytree": "2.38.5",
|
"jquery.fancytree": "2.38.5",
|
||||||
@@ -53,13 +53,13 @@
|
|||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-gpx": "2.2.0",
|
"leaflet-gpx": "2.2.0",
|
||||||
"mark.js": "8.11.1",
|
"mark.js": "8.11.1",
|
||||||
"marked": "16.4.1",
|
"marked": "16.4.2",
|
||||||
"mermaid": "11.12.1",
|
"mermaid": "11.12.1",
|
||||||
"mind-elixir": "5.3.4",
|
"mind-elixir": "5.3.5",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"panzoom": "9.4.3",
|
"panzoom": "9.4.3",
|
||||||
"preact": "10.27.2",
|
"preact": "10.27.2",
|
||||||
"react-i18next": "16.2.3",
|
"react-i18next": "16.2.4",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"svg-pan-zoom": "3.6.2",
|
"svg-pan-zoom": "3.6.2",
|
||||||
"tabulator-tables": "6.3.1",
|
"tabulator-tables": "6.3.1",
|
||||||
|
|||||||
@@ -499,6 +499,10 @@ type EventMappings = {
|
|||||||
noteIds: string[];
|
noteIds: string[];
|
||||||
};
|
};
|
||||||
refreshData: { ntxId: string | null | undefined };
|
refreshData: { ntxId: string | null | undefined };
|
||||||
|
contentSafeMarginChanged: {
|
||||||
|
top: number;
|
||||||
|
noteContext: NoteContext;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EventListener<T extends EventNames> = {
|
export type EventListener<T extends EventNames> = {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import protectedSessionService from "../services/protected_session.js";
|
|||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import LlmChatPanel from "../widgets/llm_chat_panel.js";
|
|
||||||
import toastService from "../services/toast.js";
|
import toastService from "../services/toast.js";
|
||||||
import noteCreateService from "../services/note_create.js";
|
import noteCreateService from "../services/note_create.js";
|
||||||
|
|
||||||
@@ -171,7 +170,8 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleTrayCommand() {
|
toggleTrayCommand() {
|
||||||
if (!utils.isElectron()) return;
|
if (!utils.isElectron() || options.is("disableTray")) return;
|
||||||
|
|
||||||
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
const { BrowserWindow } = utils.dynamicRequire("@electron/remote");
|
||||||
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
const windows = BrowserWindow.getAllWindows() as Electron.BaseWindow[];
|
||||||
const isVisible = windows.every((w) => w.isVisible());
|
const isVisible = windows.every((w) => w.isVisible());
|
||||||
|
|||||||
@@ -1,47 +1,49 @@
|
|||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
|
||||||
import SpacerWidget from "../widgets/spacer.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
|
||||||
import FindWidget from "../widgets/find.js";
|
|
||||||
import TocWidget from "../widgets/toc.js";
|
|
||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
|
||||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import type { AppContext } from "../components/app_context.js";
|
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
|
||||||
import { applyModals } from "./layout_commons.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
|
import ContentHeader from "../widgets/containers/content-header.js";
|
||||||
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
|
import FindWidget from "../widgets/find.js";
|
||||||
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
|
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||||
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
|
import options from "../services/options.js";
|
||||||
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
|
import SharedInfo from "../widgets/shared_info.jsx";
|
||||||
|
import SpacerWidget from "../widgets/spacer.js";
|
||||||
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
import SqlResults from "../widgets/sql_result.js";
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
import TocWidget from "../widgets/toc.js";
|
||||||
import ApiLog from "../widgets/api_log.jsx";
|
import type { AppContext } from "../components/app_context.js";
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
import SharedInfo from "../widgets/shared_info.jsx";
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import utils from "../services/utils.js";
|
||||||
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -129,12 +131,15 @@ export default class DesktopLayout {
|
|||||||
.child(<CreatePaneButton />)
|
.child(<CreatePaneButton />)
|
||||||
)
|
)
|
||||||
.child(<Ribbon />)
|
.child(<Ribbon />)
|
||||||
.child(<SharedInfo />)
|
|
||||||
.child(new WatchedFileUpdateStatusWidget())
|
.child(new WatchedFileUpdateStatusWidget())
|
||||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
|
.child(new ContentHeader()
|
||||||
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
|
.child(<SharedInfo />)
|
||||||
|
)
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(<SqlTableSchemas />)
|
.child(<SqlTableSchemas />)
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
|
import { applyModals } from "./layout_commons.js";
|
||||||
|
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
|
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||||
|
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||||
|
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import NoteTitleWidget from "../widgets/note_title.js";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
|
||||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
|
||||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
|
||||||
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
|
||||||
import type AppContext from "../components/app_context.js";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
|
||||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
|
||||||
import { applyModals } from "./layout_commons.js";
|
|
||||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
|
||||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
|
||||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
|
||||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
|
||||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
|
||||||
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
|
||||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
|
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||||
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||||
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
import NoteTitleWidget from "../widgets/note_title.js";
|
||||||
|
import ContentHeader from "../widgets/containers/content-header.js";
|
||||||
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
|
import NoteWrapperWidget from "../widgets/note_wrapper.js";
|
||||||
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
|
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
|
||||||
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||||
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
|
||||||
import SearchResult from "../widgets/search_result.jsx";
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
|
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||||
|
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
|
||||||
|
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
|
||||||
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
|
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||||
|
import type AppContext from "../components/app_context.js";
|
||||||
|
|
||||||
const MOBILE_CSS = `
|
const MOBILE_CSS = `
|
||||||
<style>
|
<style>
|
||||||
@@ -149,13 +151,16 @@ export default class MobileLayout {
|
|||||||
.child(<NoteTitleWidget />)
|
.child(<NoteTitleWidget />)
|
||||||
.child(<MobileDetailMenu />)
|
.child(<MobileDetailMenu />)
|
||||||
)
|
)
|
||||||
.child(<SharedInfoWidget />)
|
|
||||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(
|
.child(
|
||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.contentSized()
|
.contentSized()
|
||||||
|
.child(new ContentHeader()
|
||||||
|
.child(<ReadOnlyNoteInfoBar />)
|
||||||
|
.child(<SharedInfoWidget />)
|
||||||
|
)
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(<NoteList media="screen" />)
|
.child(<NoteList media="screen" />)
|
||||||
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
|
|||||||
command: "editBranchPrefix",
|
command: "editBranchPrefix",
|
||||||
keyboardShortcut: "editBranchPrefix",
|
keyboardShortcut: "editBranchPrefix",
|
||||||
uiIcon: "bx bx-rename",
|
uiIcon: "bx bx-rename",
|
||||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptionsOrHelp
|
enabled: isNotRoot && parentNotSearch && notOptionsOrHelp
|
||||||
},
|
},
|
||||||
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptionsOrHelp },
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "boxicons/css/boxicons.min.css";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--print-font-size: 11pt;
|
--print-font-size: 11pt;
|
||||||
--ck-content-color-image-caption-background: transparent !important;
|
--ck-content-color-image-caption-background: transparent !important;
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ function SingleNoteRenderer({ note, onReady }: RendererProps) {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check custom CSS.
|
||||||
|
await loadCustomCss(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
load().then(() => requestAnimationFrame(onReady))
|
load().then(() => requestAnimationFrame(onReady))
|
||||||
@@ -89,7 +92,10 @@ function CollectionRenderer({ note, onReady }: RendererProps) {
|
|||||||
ntxId="print"
|
ntxId="print"
|
||||||
highlightedTokens={null}
|
highlightedTokens={null}
|
||||||
media="print"
|
media="print"
|
||||||
onReady={onReady}
|
onReady={async () => {
|
||||||
|
await loadCustomCss(note);
|
||||||
|
onReady();
|
||||||
|
}}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,4 +108,25 @@ function Error404({ noteId }: { noteId: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCustomCss(note: FNote) {
|
||||||
|
const printCssNotes = await note.getRelationTargets("printCss");
|
||||||
|
let loadPromises: JQueryPromise<void>[] = [];
|
||||||
|
|
||||||
|
for (const printCssNote of printCssNotes) {
|
||||||
|
if (!printCssNote || (printCssNote.type !== "code" && printCssNote.mime !== "text/css")) continue;
|
||||||
|
|
||||||
|
const linkEl = document.createElement("link");
|
||||||
|
linkEl.href = `/api/notes/${printCssNote.noteId}/download`;
|
||||||
|
linkEl.rel = "stylesheet";
|
||||||
|
|
||||||
|
const promise = $.Deferred();
|
||||||
|
loadPromises.push(promise.promise());
|
||||||
|
linkEl.onload = () => promise.resolve();
|
||||||
|
|
||||||
|
document.head.appendChild(linkEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(loadPromises);
|
||||||
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe("shortcuts", () => {
|
|||||||
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
expect(matchesShortcut(event, "Shift+F1")).toBeTruthy();
|
||||||
|
|
||||||
// Special keys
|
// Special keys
|
||||||
for (const keyCode of [ "Delete", "Enter" ]) {
|
for (const keyCode of [ "Delete", "Enter", "NumpadEnter" ]) {
|
||||||
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
event = createKeyboardEvent({ key: keyCode, code: keyCode });
|
||||||
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
expect(matchesShortcut(event, keyCode), `Key ${keyCode}`).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ for (let i = 1; i <= 19; i++) {
|
|||||||
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
const KEYCODES_WITH_NO_MODIFIER = new Set([
|
||||||
"Delete",
|
"Delete",
|
||||||
"Enter",
|
"Enter",
|
||||||
|
"NumpadEnter",
|
||||||
...functionKeyCodes
|
...functionKeyCodes
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyCopyToClipboardButton($(codeBlock));
|
if (glob.device !== "print") {
|
||||||
|
applyCopyToClipboardButton($(codeBlock));
|
||||||
|
}
|
||||||
|
|
||||||
if (syntaxHighlightingEnabled) {
|
if (syntaxHighlightingEnabled) {
|
||||||
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
||||||
|
|||||||
@@ -841,7 +841,7 @@ export function arrayEqual<T>(a: T[], b: T[]) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Indexed<T extends object> = T & { index: number };
|
export type Indexed<T extends object> = T & { index: number };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given an object array, alters every object in the array to have an index field assigned to it.
|
* Given an object array, alters every object in the array to have an index field assigned to it.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
.note-detail-relation-map {
|
.note-detail-relation-map {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
padding: 10px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1104,7 +1104,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
background-color: inherit !important;
|
|
||||||
border-color: var(--main-border-color) !important;
|
border-color: var(--main-border-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1759,10 +1758,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-inline-start: 10px;
|
margin-inline-start: 10px;
|
||||||
margin-inline-end: 5px;
|
margin-inline-end: 5px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#right-pane .card-header {
|
#right-pane .card-header {
|
||||||
background: inherit;
|
|
||||||
padding: 6px 0 3px 0;
|
padding: 6px 0 3px 0;
|
||||||
width: 99%; /* to give minimal right margin */
|
width: 99%; /* to give minimal right margin */
|
||||||
background-color: var(--button-background-color);
|
background-color: var(--button-background-color);
|
||||||
@@ -1809,12 +1808,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-split {
|
.note-split {
|
||||||
|
/* Limits the maximum width of the note */
|
||||||
|
--max-content-width: var(--preferred-max-content-width);
|
||||||
|
|
||||||
margin-inline-start: auto;
|
margin-inline-start: auto;
|
||||||
margin-inline-end: auto;
|
margin-inline-end: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-split.full-content-width {
|
.note-split.full-content-width {
|
||||||
max-width: 999999px;
|
--max-content-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.close:hover {
|
button.close:hover {
|
||||||
@@ -2034,13 +2036,16 @@ body.zen #right-pane,
|
|||||||
body.zen #mobile-sidebar-wrapper,
|
body.zen #mobile-sidebar-wrapper,
|
||||||
body.zen .tab-row-container,
|
body.zen .tab-row-container,
|
||||||
body.zen .tab-row-widget,
|
body.zen .tab-row-widget,
|
||||||
|
body.zen .shared-info-widget,
|
||||||
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
|
body.zen .ribbon-container:not(:has(.classic-toolbar-widget)),
|
||||||
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
|
body.zen .ribbon-container:has(.classic-toolbar-widget) .ribbon-top-row,
|
||||||
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
|
body.zen .ribbon-container .ribbon-body:not(:has(.classic-toolbar-widget)),
|
||||||
body.zen .note-icon-widget,
|
body.zen .note-icon-widget,
|
||||||
body.zen .title-row .icon-action,
|
body.zen .title-row .icon-action,
|
||||||
|
body.zen .promoted-attributes-widget,
|
||||||
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
body.zen .floating-buttons-children > *:not(.bx-edit-alt),
|
||||||
body.zen .action-button {
|
body.zen .action-button,
|
||||||
|
body.zen .note-split:not(.type-book) .note-list-widget {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2084,12 +2089,121 @@ body.zen .note-title-widget,
|
|||||||
body.zen .note-title-widget input {
|
body.zen .note-title-widget input {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.zen #detail-container {
|
body.zen #detail-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.zen .note-split:not(.full-content-width) .scrolling-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
scroll-behavior: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split:not(.full-content-width) .note-detail {
|
||||||
|
margin: auto;
|
||||||
|
padding-bottom: 25vh;
|
||||||
|
max-width: var(--max-content-width);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split:not(.full-content-width) .scroll-padding-widget {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split.type-text {
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .title-row {
|
||||||
|
--start-color: var(--main-background-color);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(var(--start-color) 30%, transparent 100%);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (background: color-mix(in srgb, white, transparent)) {
|
||||||
|
body.zen .note-split.type-text .title-row {
|
||||||
|
--start-color: color-mix(in srgb, var(--main-background-color), transparent 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split.type-text .scrolling-container {
|
||||||
|
--padding-bottom: 130px; /* Should be enough to avoid caret being hidden by the formatting toolbar */
|
||||||
|
|
||||||
|
/* (Usually) keeps the caret above the fixed toolbar */
|
||||||
|
scroll-padding-bottom: var(--padding-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen:not(.backdrop-effects-disabled) .note-split.type-text .scrolling-container {
|
||||||
|
--padding-top: 50px; /* Should be enough to cover the title row */
|
||||||
|
|
||||||
|
padding-top: var(--padding-top);
|
||||||
|
scroll-padding-top: var(--padding-top);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed formatting toolbar */
|
||||||
|
|
||||||
|
body.zen .note-split .ribbon-container {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0; /* Hidden unless the current note split is focused */
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 100ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split:focus-within .ribbon-container {
|
||||||
|
opacity: 1; /* Show when the note split is focused */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split .ribbon-container .ribbon-body {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||||
|
margin: auto;
|
||||||
|
width: fit-content;
|
||||||
|
box-shadow: 0px 10px 20px rgba(0, 0, 0, .1);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--main-border-color);
|
||||||
|
padding: 4px;
|
||||||
|
background: var(--menu-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split .ribbon-container .classic-toolbar-widget:not(:has(> .ck-toolbar)) {
|
||||||
|
/* Hide the toolbar wrapper if the toolbar is missing */
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split:focus-within .ribbon-container .classic-toolbar-widget {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1300px) {
|
||||||
|
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||||
|
/* Set the toolbar to full with */
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_se,
|
||||||
|
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sw,
|
||||||
|
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_smw,
|
||||||
|
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_sme,
|
||||||
|
body.zen .classic-toolbar-widget .ck.ck-dropdown .ck-dropdown__panel.ck-dropdown__panel_s {
|
||||||
|
/* Force toolbar items overflow dropdowns open upwards */
|
||||||
|
top: auto;
|
||||||
|
bottom: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Content renderer */
|
/* Content renderer */
|
||||||
|
|
||||||
footer.file-footer,
|
footer.file-footer,
|
||||||
@@ -2406,7 +2520,7 @@ footer.webview-footer button {
|
|||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CK Edito */
|
/* CK Editor */
|
||||||
|
|
||||||
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
|
/* Insert text snippet: limit the width of the listed items to avoid overly long names */
|
||||||
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
|
:root body.desktop div.ck-template-form li.ck-list__item .ck-template-form__text-part > span {
|
||||||
@@ -2437,3 +2551,17 @@ iframe.print-iframe {
|
|||||||
.excalidraw.theme--dark canvas {
|
.excalidraw.theme--dark canvas {
|
||||||
--theme-filter: invert(100%) hue-rotate(180deg);
|
--theme-filter: invert(100%) hue-rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Scrolling container */
|
||||||
|
|
||||||
|
.scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolling-container > .note-detail.full-height,
|
||||||
|
.scrolling-container > .note-list-widget.full-height {
|
||||||
|
position: relative;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
--native-titlebar-background: #00000000;
|
--native-titlebar-background: #00000000;
|
||||||
--window-background-color-bgfx: transparent; /* When background effects enabled */
|
--window-background-color-bgfx: transparent; /* When background effects enabled */
|
||||||
|
|
||||||
--main-background-color: #272727;
|
--main-background-color: #242424;
|
||||||
--main-text-color: #ccc;
|
--main-text-color: #ccc;
|
||||||
--main-border-color: #454545;
|
--main-border-color: #454545;
|
||||||
--subtle-border-color: #313131;
|
--subtle-border-color: #313131;
|
||||||
@@ -166,6 +166,9 @@
|
|||||||
--protected-session-active-icon-color: #8edd8e;
|
--protected-session-active-icon-color: #8edd8e;
|
||||||
--sync-status-error-pulse-color: #f47871;
|
--sync-status-error-pulse-color: #f47871;
|
||||||
|
|
||||||
|
--center-pane-vert-layout-background-color-bgfx: #0c0c0c69;
|
||||||
|
--center-pane-horiz-layout-background-color-bgfx: #1e1e1ec7;
|
||||||
|
|
||||||
--right-pane-heading-color: gray;
|
--right-pane-heading-color: gray;
|
||||||
|
|
||||||
--root-background: var(--left-pane-background-color);
|
--root-background: var(--left-pane-background-color);
|
||||||
@@ -192,8 +195,8 @@
|
|||||||
--badge-background-color: #ffffff1a;
|
--badge-background-color: #ffffff1a;
|
||||||
--badge-text-color: var(--muted-text-color);
|
--badge-text-color: var(--muted-text-color);
|
||||||
|
|
||||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
--promoted-attribute-card-background-color: #ffffff21;
|
||||||
--promoted-attribute-card-shadow-color: #000000b3;
|
--promoted-attribute-card-shadow: none;
|
||||||
|
|
||||||
--floating-button-shadow-color: #00000080;
|
--floating-button-shadow-color: #00000080;
|
||||||
--floating-button-background-color: #494949d2;
|
--floating-button-background-color: #494949d2;
|
||||||
@@ -208,6 +211,8 @@
|
|||||||
--floating-button-hide-button-background: #00000029;
|
--floating-button-hide-button-background: #00000029;
|
||||||
--floating-button-hide-button-color: #ffffff63;
|
--floating-button-hide-button-color: #ffffff63;
|
||||||
|
|
||||||
|
--right-pane-background-color: var(--main-background-color);
|
||||||
|
--right-pane-background-color-bgfx: #0c0c0c24; /* Only for the vertical layout */
|
||||||
--right-pane-item-hover-background: #ffffff26;
|
--right-pane-item-hover-background: #ffffff26;
|
||||||
--right-pane-item-hover-color: white;
|
--right-pane-item-hover-color: white;
|
||||||
|
|
||||||
@@ -225,10 +230,9 @@
|
|||||||
--code-block-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
|
--code-block-box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
|
||||||
|
|
||||||
--card-background-color: #ffffff12;
|
--card-background-color: #ffffff12;
|
||||||
--card-background-hover-color: #3c3c3c;
|
--card-background-hover-color: #ffffff20;
|
||||||
--card-background-press-color: #464646;
|
--card-border-color: transparent;
|
||||||
--card-border-color: #222222;
|
--card-box-shadow: none;
|
||||||
--card-box-shadow: 0 0 12px rgba(0, 0, 0, 0.15);
|
|
||||||
|
|
||||||
--calendar-color: var(--menu-text-color);
|
--calendar-color: var(--menu-text-color);
|
||||||
--calendar-weekday-labels-color: var(--muted-text-color);
|
--calendar-weekday-labels-color: var(--muted-text-color);
|
||||||
@@ -295,3 +299,9 @@ body ::-webkit-calendar-picker-indicator {
|
|||||||
body .todo-list input[type="checkbox"]:not(:checked):before {
|
body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||||
border-color: var(--muted-text-color) !important;
|
border-color: var(--muted-text-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tinted-quick-edit-dialog {
|
||||||
|
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
|
||||||
|
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
|
||||||
|
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);
|
||||||
|
}
|
||||||
@@ -159,6 +159,9 @@
|
|||||||
--protected-session-active-icon-color: #16b516;
|
--protected-session-active-icon-color: #16b516;
|
||||||
--sync-status-error-pulse-color: #ff5528;
|
--sync-status-error-pulse-color: #ff5528;
|
||||||
|
|
||||||
|
--center-pane-vert-layout-background-color-bgfx: #ffffff75;
|
||||||
|
--center-pane-horiz-layout-background-color-bgfx: #ffffffd6;
|
||||||
|
|
||||||
--right-pane-heading-color: gray;
|
--right-pane-heading-color: gray;
|
||||||
|
|
||||||
--root-background: var(--left-pane-background-color);
|
--root-background: var(--left-pane-background-color);
|
||||||
@@ -180,13 +183,13 @@
|
|||||||
--inactive-tab-hover-background-color: #00000016;
|
--inactive-tab-hover-background-color: #00000016;
|
||||||
--inactive-tab-text-color: #4e4e4e;
|
--inactive-tab-text-color: #4e4e4e;
|
||||||
|
|
||||||
--alert-bar-background: #32637b29;
|
--alert-bar-background: #f9cf2b29;
|
||||||
|
|
||||||
--badge-background-color: #00000011;
|
--badge-background-color: #00000011;
|
||||||
--badge-text-color: var(--muted-text-color);
|
--badge-text-color: var(--muted-text-color);
|
||||||
|
|
||||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
--promoted-attribute-card-background-color: #00000014;
|
||||||
--promoted-attribute-card-shadow-color: #00000033;
|
--promoted-attribute-card-shadow: none;
|
||||||
|
|
||||||
--floating-button-shadow-color: #00000042;
|
--floating-button-shadow-color: #00000042;
|
||||||
--floating-button-background-color: #eaeaeacc;
|
--floating-button-background-color: #eaeaeacc;
|
||||||
@@ -207,7 +210,9 @@
|
|||||||
--new-tab-button-hover-background: white;
|
--new-tab-button-hover-background: white;
|
||||||
--new-tab-button-hover-color: black;
|
--new-tab-button-hover-color: black;
|
||||||
|
|
||||||
--right-pane-item-hover-background: #ececec;
|
--right-pane-background-color: var(--main-background-color);
|
||||||
|
--right-pane-background-color-bgfx: #ffffff9e; /* Only for the vertical layout */
|
||||||
|
--right-pane-item-hover-background: #00000013;
|
||||||
--right-pane-item-hover-color: inherit;
|
--right-pane-item-hover-color: inherit;
|
||||||
|
|
||||||
--scrollbar-thumb-color: #0000005c;
|
--scrollbar-thumb-color: #0000005c;
|
||||||
@@ -223,12 +228,11 @@
|
|||||||
|
|
||||||
--code-block-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
|
--code-block-box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
--card-background-color: var(--accented-background-color);
|
--card-background-color: #0000000d;
|
||||||
--card-background-hover-color: #f9f9f9;
|
--card-background-hover-color: #0000001c;
|
||||||
--card-background-press-color: #efefef;
|
--card-border-color: transparent;
|
||||||
--card-border-color: #eaeaea;
|
|
||||||
--card-shadow-color: rgba(0, 0, 0, 0.1);
|
--card-shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
--card-box-shadow: 0 0 12px var(--card-shadow-color);
|
--card-box-shadow: none;
|
||||||
|
|
||||||
--calendar-color: var(--menu-text-color);
|
--calendar-color: var(--menu-text-color);
|
||||||
--calendar-weekday-labels-color: var(--muted-text-color);
|
--calendar-weekday-labels-color: var(--muted-text-color);
|
||||||
@@ -271,3 +275,9 @@
|
|||||||
* This value is unset for gray tones. */
|
* This value is unset for gray tones. */
|
||||||
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tinted-quick-edit-dialog {
|
||||||
|
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
|
||||||
|
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
|
||||||
|
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);
|
||||||
|
}
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
|
|
||||||
/* Theme capabilities */
|
/* Theme capabilities */
|
||||||
--tab-note-icons: true;
|
--tab-note-icons: true;
|
||||||
|
--allow-background-effects: true;
|
||||||
|
|
||||||
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
|
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
|
||||||
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
* the color is adjusted based on the current color scheme (light or dark). The lightness
|
||||||
@@ -131,7 +132,8 @@ body.mobile .dropdown-menu .dropdown-menu {
|
|||||||
|
|
||||||
body.desktop .dropdown-menu::before,
|
body.desktop .dropdown-menu::before,
|
||||||
:root .ck.ck-dropdown__panel::before,
|
:root .ck.ck-dropdown__panel::before,
|
||||||
:root .excalidraw .popover::before {
|
:root .excalidraw .popover::before,
|
||||||
|
body.zen .note-split .ribbon-container .classic-toolbar-widget::before {
|
||||||
content: "";
|
content: "";
|
||||||
backdrop-filter: var(--dropdown-backdrop-filter);
|
backdrop-filter: var(--dropdown-backdrop-filter);
|
||||||
border-radius: var(--dropdown-border-radius);
|
border-radius: var(--dropdown-border-radius);
|
||||||
@@ -485,13 +487,21 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
--note-list-vertical-padding: 15px;
|
--note-list-vertical-padding: 15px;
|
||||||
background-color: var(--card-background-color);
|
background-color: var(--card-background-color);
|
||||||
border: 1px solid var(--card-border-color) !important;
|
border: 1px solid var(--card-border-color) !important;
|
||||||
box-shadow: 2px 3px 4px var(--card-shadow-color);
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 5px 10px 5px 0;
|
margin: 5px 10px 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root .note-list .note-book-card:hover {
|
||||||
|
background-color: var(--card-background-hover-color);
|
||||||
|
transition: background-color 200ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .note-list.grid-view .note-book-card:active {
|
||||||
|
transform: scale(.98);
|
||||||
|
}
|
||||||
|
|
||||||
.note-list.list-view .note-book-card {
|
.note-list.list-view .note-book-card {
|
||||||
box-shadow: 0 0 3px var(--card-shadow-color);
|
box-shadow: 0 0 3px var(--card-shadow-color);
|
||||||
}
|
}
|
||||||
@@ -500,10 +510,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-list-wrapper .note-book-card:active {
|
|
||||||
background-color: var(--card-background-press-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.note-list-wrapper .note-book-card a {
|
.note-list-wrapper .note-book-card a {
|
||||||
color: inherit !important;
|
color: inherit !important;
|
||||||
}
|
}
|
||||||
@@ -585,7 +591,6 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.note-list.grid-view .note-book-card:hover {
|
.note-list.grid-view .note-book-card:hover {
|
||||||
background: var(--card-background-color) !important;
|
|
||||||
filter: contrast(105%);
|
filter: contrast(105%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,11 +258,6 @@
|
|||||||
border-inline-start: 1px solid var(--ck-color-toolbar-border);
|
border-inline-start: 1px solid var(--ck-color-toolbar-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The last separator of the toolbar */
|
|
||||||
:root .classic-toolbar-widget .ck.ck-toolbar__separator:last-of-type {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Heading dropdown */
|
/* Heading dropdown */
|
||||||
|
|
||||||
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
|
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
|
||||||
@@ -680,3 +675,16 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
|
|||||||
.ck-content a.reference-link > span {
|
.ck-content a.reference-link > span {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read-only text content
|
||||||
|
*/
|
||||||
|
|
||||||
|
.note-detail-readonly-text:focus-visible {
|
||||||
|
outline: 2px solid var(--input-focus-outline-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-list-widget {
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
||||||
.sql-table-schemas-widget .sql-table-schemas button:active,
|
.sql-table-schemas-widget .sql-table-schemas button:active,
|
||||||
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
|
.sql-table-schemas-widget .sql-table-schemas button:focus-visible {
|
||||||
--background: var(--card-background-press-color);
|
--background: var(--card-background-hover-color);
|
||||||
--color: var(--main-text-color);
|
--color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,8 +123,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* The container */
|
/* The container */
|
||||||
div.note-detail-empty {
|
|
||||||
max-width: 70%;
|
.note-split.empty-note {
|
||||||
|
--max-content-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-split.empty-note div.note-detail {
|
||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +152,7 @@ div.note-detail-empty {
|
|||||||
--options-card-min-width: 500px;
|
--options-card-min-width: 500px;
|
||||||
--options-card-max-width: 900px;
|
--options-card-max-width: 900px;
|
||||||
--options-card-padding: 17px;
|
--options-card-padding: 17px;
|
||||||
--options-title-font-size: 1rem;
|
--options-title-font-size: .75rem;
|
||||||
--options-title-offset: 13px;
|
--options-title-offset: 13px;
|
||||||
}
|
}
|
||||||
/* Create a gap at the top of the option pages */
|
/* Create a gap at the top of the option pages */
|
||||||
@@ -173,16 +177,19 @@ div.note-detail-empty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.options-section:not(.tn-no-card) {
|
.options-section:not(.tn-no-card) {
|
||||||
margin: auto;
|
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--card-border-color) !important;
|
|
||||||
box-shadow: var(--card-box-shadow);
|
box-shadow: var(--card-box-shadow);
|
||||||
|
border: 1px solid var(--card-border-color) !important;
|
||||||
|
border-radius: 8px;
|
||||||
background: var(--card-background-color);
|
background: var(--card-background-color);
|
||||||
padding: var(--options-card-padding);
|
padding: var(--options-card-padding);
|
||||||
margin-bottom: calc(var(--options-title-offset) + 26px) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body.desktop .option-section:not(.tn-no-card) {
|
body.prefers-centered-content .options-section:not(.tn-no-card) {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.desktop .options-section:not(.tn-no-card) {
|
||||||
min-width: var(--options-card-min-width);
|
min-width: var(--options-card-min-width);
|
||||||
max-width: var(--options-card-max-width);
|
max-width: var(--options-card-max-width);
|
||||||
}
|
}
|
||||||
@@ -193,9 +200,16 @@ body.desktop .option-section:not(.tn-no-card) {
|
|||||||
padding-bottom: var(--default-padding);
|
padding-bottom: var(--default-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.options-section:not(.tn-no-card) h4,
|
||||||
|
.options-section:not(.tn-no-card) h5 {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .4pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.options-section:not(.tn-no-card) h4 {
|
.options-section:not(.tn-no-card) h4 {
|
||||||
font-size: var(--options-title-font-size);
|
font-size: var(--options-title-font-size);
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: var(--launcher-pane-text-color);
|
color: var(--launcher-pane-text-color);
|
||||||
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
|
margin-top: calc(-1 * var(--options-card-padding) - var(--options-title-font-size) - var(--options-title-offset)) !important;
|
||||||
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;
|
margin-bottom: calc(var(--options-title-offset) + var(--options-card-padding)) !important;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
div.promoted-attributes-container {
|
div.promoted-attributes-container {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
margin-inline-start: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--dropdown-backdrop-filter: blur(10px) saturate(6);
|
--dropdown-backdrop-filter: blur(20px) saturate(6);
|
||||||
--dropdown-border-radius: 10px;
|
--dropdown-border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,30 +35,53 @@ body.mobile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* #region Mica */
|
/* #region Mica */
|
||||||
|
|
||||||
body.background-effects.platform-win32 {
|
body.background-effects.platform-win32 {
|
||||||
|
/* Quirk: --background-material is read before "theme-supports-background-effects" class
|
||||||
|
* is applied. Apply the matterial even if the theme doesn't support it. */
|
||||||
--background-material: tabbed;
|
--background-material: tabbed;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.background-effects.theme-supports-background-effects.platform-win32 {
|
||||||
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
|
--launcher-pane-horiz-border-color: var(--launcher-pane-horiz-border-color-bgfx);
|
||||||
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
|
--launcher-pane-horiz-background-color: var(--launcher-pane-horiz-background-color-bgfx);
|
||||||
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
|
--launcher-pane-vert-background-color: var(--launcher-pane-vert-background-color-bgfx);
|
||||||
--tab-background-color: var(--window-background-color-bgfx);
|
--tab-background-color: var(--window-background-color-bgfx);
|
||||||
--new-tab-button-background: var(--window-background-color-bgfx);
|
--new-tab-button-background: var(--window-background-color-bgfx);
|
||||||
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
|
--active-tab-background-color: var(--launcher-pane-horiz-background-color);
|
||||||
|
--root-background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.background-effects.platform-win32.layout-vertical {
|
body.background-effects.platform-win32.layout-vertical {
|
||||||
--left-pane-background-color: var(--window-background-color-bgfx);
|
|
||||||
--background-material: mica;
|
--background-material: mica;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.background-effects.platform-win32,
|
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical {
|
||||||
body.background-effects.platform-win32 #root-widget {
|
--left-pane-background-color: var(--window-background-color-bgfx);
|
||||||
|
--center-pane-background-color-bgfx: var(--center-pane-vert-layout-background-color-bgfx);
|
||||||
|
--right-pane-background-color: var(--right-pane-background-color-bgfx);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal {
|
||||||
|
--center-pane-background-color-bgfx: var(--center-pane-horiz-layout-background-color-bgfx);
|
||||||
|
--gutter-color: var(--left-pane-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.background-effects.theme-supports-background-effects.platform-win32,
|
||||||
|
body.background-effects.theme-supports-background-effects.platform-win32 #root-widget {
|
||||||
background: var(--window-background-color-bgfx) !important;
|
background: var(--window-background-color-bgfx) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
body.background-effects.theme-supports-background-effects.platform-win32.layout-horizontal #horizontal-main-container,
|
||||||
body.background-effects.platform-win32.layout-vertical #vertical-main-container {
|
body.background-effects.theme-supports-background-effects.platform-win32.layout-vertical #vertical-main-container {
|
||||||
background-color: var(--root-background);
|
background-color: var(--root-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Note split with background effects */
|
||||||
|
body.background-effects.theme-supports-background-effects.platform-win32 #center-pane .note-split.bgfx {
|
||||||
|
--note-split-background-color: var(--center-pane-background-color-bgfx);
|
||||||
|
}
|
||||||
|
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
/* Matches when the left pane is collapsed */
|
/* Matches when the left pane is collapsed */
|
||||||
@@ -72,9 +95,21 @@ body.layout-vertical #horizontal-main-container.left-pane-hidden #launcher-pane.
|
|||||||
border-inline-end: 2px solid var(--left-pane-collapsed-border-color);
|
border-inline-end: 2px solid var(--left-pane-collapsed-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body.background-effects.zen #root-widget {
|
/*
|
||||||
--main-background-color: transparent;
|
* Zen mode
|
||||||
--root-background: transparent;
|
*/
|
||||||
|
|
||||||
|
@keyframes zen-formatting-toolbar-entrance {
|
||||||
|
from {
|
||||||
|
transform: translateY(200%);
|
||||||
|
} to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .note-split .ribbon-container .classic-toolbar-widget {
|
||||||
|
position: relative;
|
||||||
|
animation: zen-formatting-toolbar-entrance 300ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1171,23 +1206,18 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
|||||||
* CENTER PANE
|
* CENTER PANE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#center-pane {
|
/* The first visible note split */
|
||||||
background: var(--main-background-color);
|
.vertical-layout #center-pane .note-split:not(.visible ~ .visible) {
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-layout #center-pane {
|
|
||||||
border-radius: var(--center-pane-border-radius) 0 0 0;
|
border-radius: var(--center-pane-border-radius) 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-split {
|
#center-pane .note-split {
|
||||||
padding-top: 2px;
|
padding-top: 2px;
|
||||||
animation: note-entrance 100ms linear;
|
background-color: var(--note-split-background-color, var(--main-background-color));
|
||||||
/* will-change: opacity; -- causes some weird artifacts to the note menu in split view */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.split-note-container-widget > .gutter {
|
body:not(.background-effects) #center-pane .note-split {
|
||||||
background: var(--root-background) !important;
|
animation: note-entrance 100ms linear;
|
||||||
transition: background 150ms ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1200,9 +1230,9 @@ body.layout-vertical .tab-row-widget-is-sorting .note-tab.note-tab-is-dragging .
|
|||||||
|
|
||||||
@keyframes note-entrance {
|
@keyframes note-entrance {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
filter: opacity(0);
|
||||||
} to {
|
} to {
|
||||||
opacity: 1;
|
filter: opacity(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,8 +1358,7 @@ div.promoted-attribute-cell {
|
|||||||
--pa-card-padding-inline-end: 2px;
|
--pa-card-padding-inline-end: 2px;
|
||||||
--input-background-color: transparent;
|
--input-background-color: transparent;
|
||||||
|
|
||||||
box-shadow: 1px 1px 2px var(--promoted-attribute-card-shadow-color);
|
box-shadow: var(--promoted-attribute-card-shadow);
|
||||||
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -1716,7 +1745,7 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#right-pane {
|
#right-pane {
|
||||||
background: var(--main-background-color);
|
background: var(--right-pane-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
#right-pane div.card-header {
|
#right-pane div.card-header {
|
||||||
|
|||||||
@@ -520,9 +520,7 @@
|
|||||||
"max_content_width": {
|
"max_content_width": {
|
||||||
"max_width_unit": "بكسل",
|
"max_width_unit": "بكسل",
|
||||||
"title": "عرض المحتوى",
|
"title": "عرض المحتوى",
|
||||||
"reload_button": "اعادة تحميل الواجهة",
|
"max_width_label": "اقصى عرض للمحتوى"
|
||||||
"max_width_label": "اقصى عرض للمحتوى",
|
|
||||||
"reload_description": "تغييرات من خيارات المظهر"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"enabled": "مفعل",
|
"enabled": "مفعل",
|
||||||
|
|||||||
@@ -39,7 +39,10 @@
|
|||||||
"help_on_tree_prefix": "有关树前缀的帮助",
|
"help_on_tree_prefix": "有关树前缀的帮助",
|
||||||
"prefix": "前缀: ",
|
"prefix": "前缀: ",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"branch_prefix_saved": "分支前缀已保存。"
|
"branch_prefix_saved": "分支前缀已保存。",
|
||||||
|
"edit_branch_prefix_multiple": "编辑 {{count}} 个分支的前缀",
|
||||||
|
"branch_prefix_saved_multiple": "已为 {{count}} 个分支保存分支前缀。",
|
||||||
|
"affected_branches": "受影响的分支 {{count}}:"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "批量操作",
|
"bulk_actions": "批量操作",
|
||||||
@@ -1107,9 +1110,6 @@
|
|||||||
"title": "内容宽度",
|
"title": "内容宽度",
|
||||||
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
|
"default_description": "Trilium默认会限制内容的最大宽度以提高在宽屏中全屏时的可读性。",
|
||||||
"max_width_label": "内容最大宽度(像素)",
|
"max_width_label": "内容最大宽度(像素)",
|
||||||
"apply_changes_description": "要应用内容宽度更改,请点击",
|
|
||||||
"reload_button": "重载前端",
|
|
||||||
"reload_description": "来自外观选项的更改",
|
|
||||||
"max_width_unit": "像素"
|
"max_width_unit": "像素"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
@@ -1917,7 +1917,7 @@
|
|||||||
},
|
},
|
||||||
"custom_date_time_format": {
|
"custom_date_time_format": {
|
||||||
"title": "自定义日期/时间格式",
|
"title": "自定义日期/时间格式",
|
||||||
"description": "通过<shortcut />或工具栏的方式可自定义日期和时间格式,有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
"description": "自定义通过 <shortcut /> 或工具栏插入的日期和时间格式。有关日期/时间格式字符串中各个字符的含义,请参阅<doc>Day.js docs</doc>。",
|
||||||
"format_string": "日期/时间格式字符串:",
|
"format_string": "日期/时间格式字符串:",
|
||||||
"formatted_time": "格式化后日期/时间:"
|
"formatted_time": "格式化后日期/时间:"
|
||||||
},
|
},
|
||||||
@@ -2079,5 +2079,8 @@
|
|||||||
"edit-slide": "编辑此幻灯片",
|
"edit-slide": "编辑此幻灯片",
|
||||||
"start-presentation": "开始演示",
|
"start-presentation": "开始演示",
|
||||||
"slide-overview": "切换幻灯片概览"
|
"slide-overview": "切换幻灯片概览"
|
||||||
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "删除笔记..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1104,9 +1104,6 @@
|
|||||||
"title": "Inhaltsbreite",
|
"title": "Inhaltsbreite",
|
||||||
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
|
"default_description": "Trilium begrenzt standardmäßig die maximale Inhaltsbreite, um die Lesbarkeit für maximierte Bildschirme auf Breitbildschirmen zu verbessern.",
|
||||||
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
"max_width_label": "Maximale Inhaltsbreite in Pixel",
|
||||||
"apply_changes_description": "Um Änderungen an der Inhaltsbreite anzuwenden, klicke auf",
|
|
||||||
"reload_button": "Frontend neu laden",
|
|
||||||
"reload_description": "Änderungen an den Darstellungsoptionen",
|
|
||||||
"max_width_unit": "Pixel"
|
"max_width_unit": "Pixel"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
|
|||||||
@@ -36,10 +36,13 @@
|
|||||||
},
|
},
|
||||||
"branch_prefix": {
|
"branch_prefix": {
|
||||||
"edit_branch_prefix": "Edit branch prefix",
|
"edit_branch_prefix": "Edit branch prefix",
|
||||||
|
"edit_branch_prefix_multiple": "Edit branch prefix for {{count}} branches",
|
||||||
"help_on_tree_prefix": "Help on Tree prefix",
|
"help_on_tree_prefix": "Help on Tree prefix",
|
||||||
"prefix": "Prefix: ",
|
"prefix": "Prefix: ",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"branch_prefix_saved": "Branch prefix has been saved."
|
"branch_prefix_saved": "Branch prefix has been saved.",
|
||||||
|
"branch_prefix_saved_multiple": "Branch prefix has been saved for {{count}} branches.",
|
||||||
|
"affected_branches": "Affected branches ({{count}}):"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "Bulk actions",
|
"bulk_actions": "Bulk actions",
|
||||||
@@ -1108,9 +1111,7 @@
|
|||||||
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
|
"default_description": "Trilium by default limits max content width to improve readability for maximized screens on wide screens.",
|
||||||
"max_width_label": "Max content width",
|
"max_width_label": "Max content width",
|
||||||
"max_width_unit": "pixels",
|
"max_width_unit": "pixels",
|
||||||
"apply_changes_description": "To apply content width changes, click on",
|
"centerContent": "Keep content centered"
|
||||||
"reload_button": "reload frontend",
|
|
||||||
"reload_description": "changes from appearance options"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Native Title Bar (requires app restart)",
|
"title": "Native Title Bar (requires app restart)",
|
||||||
@@ -1636,6 +1637,12 @@
|
|||||||
"shared_locally": "This note is shared locally on {{- link}}.",
|
"shared_locally": "This note is shared locally on {{- link}}.",
|
||||||
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
|
"help_link": "For help visit <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki</a>."
|
||||||
},
|
},
|
||||||
|
"read-only-info": {
|
||||||
|
"read-only-note": "Currently viewing a read-only note.",
|
||||||
|
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
|
||||||
|
"auto-read-only-learn-more": "Learn more",
|
||||||
|
"edit-note": "Edit note"
|
||||||
|
},
|
||||||
"note_types": {
|
"note_types": {
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
@@ -2034,6 +2041,9 @@
|
|||||||
"start-presentation": "Start presentation",
|
"start-presentation": "Start presentation",
|
||||||
"slide-overview": "Toggle an overview of the slides"
|
"slide-overview": "Toggle an overview of the slides"
|
||||||
},
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "Delete note..."
|
||||||
|
},
|
||||||
"command_palette": {
|
"command_palette": {
|
||||||
"tree-action-name": "Tree: {{name}}",
|
"tree-action-name": "Tree: {{name}}",
|
||||||
"export_note_title": "Export Note",
|
"export_note_title": "Export Note",
|
||||||
|
|||||||
@@ -1107,10 +1107,7 @@
|
|||||||
"title": "Ancho del contenido",
|
"title": "Ancho del contenido",
|
||||||
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
|
"default_description": "Trilium limita de forma predeterminada el ancho máximo del contenido para mejorar la legibilidad de ventanas maximizadas en pantallas anchas.",
|
||||||
"max_width_label": "Ancho máximo del contenido en píxeles",
|
"max_width_label": "Ancho máximo del contenido en píxeles",
|
||||||
"max_width_unit": "píxeles",
|
"max_width_unit": "píxeles"
|
||||||
"apply_changes_description": "Para aplicar cambios en el ancho del contenido, haga clic en",
|
|
||||||
"reload_button": "recargar la interfaz",
|
|
||||||
"reload_description": "cambios desde las opciones de apariencia"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Barra de título nativa (requiere reiniciar la aplicación)",
|
"title": "Barra de título nativa (requiere reiniciar la aplicación)",
|
||||||
@@ -1592,7 +1589,7 @@
|
|||||||
"tree-context-menu": {
|
"tree-context-menu": {
|
||||||
"open-in-a-new-tab": "Abrir en nueva pestaña",
|
"open-in-a-new-tab": "Abrir en nueva pestaña",
|
||||||
"open-in-a-new-split": "Abrir en nueva división",
|
"open-in-a-new-split": "Abrir en nueva división",
|
||||||
"insert-note-after": "Insertar nota después de",
|
"insert-note-after": "Insertar nota contigua",
|
||||||
"insert-child-note": "Insertar subnota",
|
"insert-child-note": "Insertar subnota",
|
||||||
"delete": "Eliminar",
|
"delete": "Eliminar",
|
||||||
"search-in-subtree": "Buscar en subárbol",
|
"search-in-subtree": "Buscar en subárbol",
|
||||||
|
|||||||
@@ -1106,9 +1106,6 @@
|
|||||||
"title": "Largeur du contenu",
|
"title": "Largeur du contenu",
|
||||||
"default_description": "Trilium limite par défaut la largeur maximale du contenu pour améliorer la lisibilité sur des écrans larges.",
|
"default_description": "Trilium limite par défaut la largeur maximale du contenu pour améliorer la lisibilité sur des écrans larges.",
|
||||||
"max_width_label": "Largeur maximale du contenu en pixels",
|
"max_width_label": "Largeur maximale du contenu en pixels",
|
||||||
"apply_changes_description": "Pour appliquer les modifications de largeur du contenu, cliquez sur",
|
|
||||||
"reload_button": "recharger l'interface",
|
|
||||||
"reload_description": "changements par rapport aux options d'apparence",
|
|
||||||
"max_width_unit": "Pixels"
|
"max_width_unit": "Pixels"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
|
|||||||
@@ -109,7 +109,8 @@
|
|||||||
"export_type_single": "Solo questa nota, senza le sottostanti",
|
"export_type_single": "Solo questa nota, senza le sottostanti",
|
||||||
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
|
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
|
||||||
"opml_version_1": "OPML v.1.0 - solo testo semplice",
|
"opml_version_1": "OPML v.1.0 - solo testo semplice",
|
||||||
"opml_version_2": "OPML v2.0 - supporta anche HTML"
|
"opml_version_2": "OPML v2.0 - supporta anche HTML",
|
||||||
|
"share-format": "HTML per la pubblicazione sul web - utilizza lo stesso tema utilizzato per le note condivise, ma può essere pubblicato come sito web statico."
|
||||||
},
|
},
|
||||||
"password_not_set": {
|
"password_not_set": {
|
||||||
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
"body1": "Le note protette sono crittografate utilizzando una password utente, ma la password non è stata ancora impostata.",
|
||||||
@@ -1569,10 +1570,7 @@
|
|||||||
"title": "Larghezza del contenuto",
|
"title": "Larghezza del contenuto",
|
||||||
"default_description": "Per impostazione predefinita, Trilium limita la larghezza massima del contenuto per migliorare la leggibilità sugli schermi più grandi.",
|
"default_description": "Per impostazione predefinita, Trilium limita la larghezza massima del contenuto per migliorare la leggibilità sugli schermi più grandi.",
|
||||||
"max_width_label": "Larghezza massima del contenuto",
|
"max_width_label": "Larghezza massima del contenuto",
|
||||||
"max_width_unit": "pixel",
|
"max_width_unit": "pixel"
|
||||||
"apply_changes_description": "Per applicare le modifiche alla larghezza del contenuto, fare clic su",
|
|
||||||
"reload_button": "ricarica frontend",
|
|
||||||
"reload_description": "modifiche dalle opzioni di aspetto"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Barra del titolo nativa (richiede il riavvio dell'app)",
|
"title": "Barra del titolo nativa (richiede il riavvio dell'app)",
|
||||||
|
|||||||
@@ -39,7 +39,10 @@
|
|||||||
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
"edit_branch_prefix": "ブランチ接頭辞の編集",
|
||||||
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
"help_on_tree_prefix": "ツリー接頭辞に関するヘルプ",
|
||||||
"prefix": "接頭辞: ",
|
"prefix": "接頭辞: ",
|
||||||
"branch_prefix_saved": "ブランチの接頭辞が保存されました。"
|
"branch_prefix_saved": "ブランチの接頭辞が保存されました。",
|
||||||
|
"edit_branch_prefix_multiple": "{{count}} ブランチのブランチ接頭辞を編集",
|
||||||
|
"branch_prefix_saved_multiple": "{{count}} 個のブランチのブランチ接頭辞が保存されました。",
|
||||||
|
"affected_branches": "影響を受けるブランチ {{count}}:"
|
||||||
},
|
},
|
||||||
"global_menu": {
|
"global_menu": {
|
||||||
"menu": "メニュー",
|
"menu": "メニュー",
|
||||||
@@ -830,13 +833,10 @@
|
|||||||
"theme_defined": "テーマが定義されました"
|
"theme_defined": "テーマが定義されました"
|
||||||
},
|
},
|
||||||
"max_content_width": {
|
"max_content_width": {
|
||||||
"reload_button": "フロントエンドをリロード",
|
|
||||||
"title": "コンテンツ幅",
|
"title": "コンテンツ幅",
|
||||||
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
"default_description": "Triliumは、ワイドスクリーンで最大化された画面での可読性を向上させるために、デフォルトでコンテンツの最大幅を制限しています。",
|
||||||
"max_width_label": "最大コンテンツ幅",
|
"max_width_label": "最大コンテンツ幅",
|
||||||
"max_width_unit": "ピクセル",
|
"max_width_unit": "ピクセル"
|
||||||
"apply_changes_description": "コンテンツ幅の変更を適用するには、クリックしてください",
|
|
||||||
"reload_description": "外観設定から変更"
|
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"title": "アプリのテーマ",
|
"title": "アプリのテーマ",
|
||||||
@@ -2079,5 +2079,8 @@
|
|||||||
"edit-slide": "このスライドを編集",
|
"edit-slide": "このスライドを編集",
|
||||||
"start-presentation": "プレゼンテーションを開始",
|
"start-presentation": "プレゼンテーションを開始",
|
||||||
"slide-overview": "スライドの概要を切り替え"
|
"slide-overview": "スライドの概要を切り替え"
|
||||||
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "ノートを削除..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1464,10 +1464,7 @@
|
|||||||
"title": "Szerokość zawartości",
|
"title": "Szerokość zawartości",
|
||||||
"default_description": "Trilium domyślnie ogranicza maksymalną szerokość zawartości, aby poprawić czytelność na zmaksymalizowanych ekranach o dużej szerokości.",
|
"default_description": "Trilium domyślnie ogranicza maksymalną szerokość zawartości, aby poprawić czytelność na zmaksymalizowanych ekranach o dużej szerokości.",
|
||||||
"max_width_label": "Maksymalna szerokość zawartości",
|
"max_width_label": "Maksymalna szerokość zawartości",
|
||||||
"max_width_unit": "piksele",
|
"max_width_unit": "piksele"
|
||||||
"apply_changes_description": "Aby zastosować zmiany szerokości zawartości, kliknij na",
|
|
||||||
"reload_button": "przeładuj frontend",
|
|
||||||
"reload_description": "zmiany z opcji wyglądu"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Natywny pasek tytułu (wymaga ponownego uruchomienia aplikacji)",
|
"title": "Natywny pasek tytułu (wymaga ponownego uruchomienia aplikacji)",
|
||||||
|
|||||||
@@ -1082,10 +1082,7 @@
|
|||||||
"title": "Largura do Conteúdo",
|
"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.",
|
"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_label": "Largura máxima do conteúdo",
|
||||||
"max_width_unit": "pixels",
|
"max_width_unit": "pixels"
|
||||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
|
||||||
"reload_button": "recarregar frontend",
|
|
||||||
"reload_description": "alterações de opções de aparência"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Barra de Título Nativa (requer recarregar a app)",
|
"title": "Barra de Título Nativa (requer recarregar a app)",
|
||||||
|
|||||||
@@ -1304,9 +1304,6 @@
|
|||||||
"title": "Largura do Conteúdo",
|
"title": "Largura do Conteúdo",
|
||||||
"max_width_label": "Largura máxima do conteúdo",
|
"max_width_label": "Largura máxima do conteúdo",
|
||||||
"max_width_unit": "pixels",
|
"max_width_unit": "pixels",
|
||||||
"apply_changes_description": "Para aplicar as alterações de largura do conteúdo, clique em",
|
|
||||||
"reload_button": "recarregar frontend",
|
|
||||||
"reload_description": "alterações de opções de aparência",
|
|
||||||
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
|
"default_description": "Por padrão, o Trilium limita a largura máxima do conteúdo para melhorar a legibilidade em janelas maximizadas em telas wide."
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
|
|||||||
@@ -796,12 +796,9 @@
|
|||||||
"modal_body_text": "Din cauza limitărilor la nivel de navigator, nu este posibilă citirea clipboard-ului din JavaScript. Inserați Markdown-ul pentru a-l importa în caseta de mai jos și dați clic pe butonul Import"
|
"modal_body_text": "Din cauza limitărilor la nivel de navigator, nu este posibilă citirea clipboard-ului din JavaScript. Inserați Markdown-ul pentru a-l importa în caseta de mai jos și dați clic pe butonul Import"
|
||||||
},
|
},
|
||||||
"max_content_width": {
|
"max_content_width": {
|
||||||
"apply_changes_description": "Pentru a aplica schimbările de lățime a conținutului, dați click pe",
|
|
||||||
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
|
"default_description": "În mod implicit Trilium limitează lățimea conținutului pentru a îmbunătăți lizibilitatea pentru ferestrele maximizate pe ecrane late.",
|
||||||
"max_width_label": "Lungimea maximă a conținutului",
|
"max_width_label": "Lungimea maximă a conținutului",
|
||||||
"max_width_unit": "pixeli",
|
"max_width_unit": "pixeli",
|
||||||
"reload_button": "reîncarcă interfața",
|
|
||||||
"reload_description": "schimbări din opțiunile de afișare",
|
|
||||||
"title": "Lățime conținut"
|
"title": "Lățime conținut"
|
||||||
},
|
},
|
||||||
"mobile_detail_menu": {
|
"mobile_detail_menu": {
|
||||||
|
|||||||
@@ -1203,11 +1203,8 @@
|
|||||||
"max_content_width": {
|
"max_content_width": {
|
||||||
"max_width_unit": "пикселей",
|
"max_width_unit": "пикселей",
|
||||||
"title": "Ширина контентной области",
|
"title": "Ширина контентной области",
|
||||||
"reload_button": "перезагрузить интерфейс",
|
|
||||||
"default_description": "Trilium по умолчанию ограничивает максимальную ширину контента, чтобы улучшить читаемость на широких экранах.",
|
"default_description": "Trilium по умолчанию ограничивает максимальную ширину контента, чтобы улучшить читаемость на широких экранах.",
|
||||||
"max_width_label": "Максимальная ширина контентной области",
|
"max_width_label": "Максимальная ширина контентной области"
|
||||||
"apply_changes_description": "Чтобы применить изменения, нажмите на",
|
|
||||||
"reload_description": "изменения в параметрах внешнего вида"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"enabled": "включено",
|
"enabled": "включено",
|
||||||
|
|||||||
@@ -39,7 +39,10 @@
|
|||||||
"help_on_tree_prefix": "有關樹前綴的說明",
|
"help_on_tree_prefix": "有關樹前綴的說明",
|
||||||
"prefix": "前綴: ",
|
"prefix": "前綴: ",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
"branch_prefix_saved": "已儲存分支前綴。"
|
"branch_prefix_saved": "已儲存分支前綴。",
|
||||||
|
"edit_branch_prefix_multiple": "編輯 {{count}} 個分支的前綴",
|
||||||
|
"branch_prefix_saved_multiple": "已為 {{count}} 個分支儲存分支前綴。",
|
||||||
|
"affected_branches": "受影響的分支 ({{count}}):"
|
||||||
},
|
},
|
||||||
"bulk_actions": {
|
"bulk_actions": {
|
||||||
"bulk_actions": "批次操作",
|
"bulk_actions": "批次操作",
|
||||||
@@ -1104,9 +1107,6 @@
|
|||||||
"title": "內容寬度",
|
"title": "內容寬度",
|
||||||
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
|
"default_description": "Trilium 預設會限制內容的最大寬度以提高在寬螢幕中全螢幕時的可讀性。",
|
||||||
"max_width_label": "內容最大寬度(像素)",
|
"max_width_label": "內容最大寬度(像素)",
|
||||||
"apply_changes_description": "要套用內容寬度更改,請點擊",
|
|
||||||
"reload_button": "重新載入前端",
|
|
||||||
"reload_description": "來自外觀選項的更改",
|
|
||||||
"max_width_unit": "像素"
|
"max_width_unit": "像素"
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
@@ -2079,5 +2079,8 @@
|
|||||||
"edit-slide": "編輯此投影片",
|
"edit-slide": "編輯此投影片",
|
||||||
"start-presentation": "開始簡報",
|
"start-presentation": "開始簡報",
|
||||||
"slide-overview": "切換投影片概覽"
|
"slide-overview": "切換投影片概覽"
|
||||||
|
},
|
||||||
|
"calendar_view": {
|
||||||
|
"delete_note": "刪除筆記…"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1204,10 +1204,7 @@
|
|||||||
"title": "Ширина вмісту",
|
"title": "Ширина вмісту",
|
||||||
"default_description": "Trilium за замовчуванням обмежує максимальну ширину вмісту, щоб поліпшити читабельність на широкоформатних екранах у режимі максимального розширення.",
|
"default_description": "Trilium за замовчуванням обмежує максимальну ширину вмісту, щоб поліпшити читабельність на широкоформатних екранах у режимі максимального розширення.",
|
||||||
"max_width_label": "Максимальна ширина вмісту",
|
"max_width_label": "Максимальна ширина вмісту",
|
||||||
"max_width_unit": "пікселів",
|
"max_width_unit": "пікселів"
|
||||||
"apply_changes_description": "Щоб застосувати зміни ширини вмісту, натисніть на",
|
|
||||||
"reload_button": "перезавантажити інтерфейс",
|
|
||||||
"reload_description": "зміни в параметрах зовнішнього вигляду"
|
|
||||||
},
|
},
|
||||||
"native_title_bar": {
|
"native_title_bar": {
|
||||||
"title": "Нативний рядок заголовка (потрібен перезапуск)",
|
"title": "Нативний рядок заголовка (потрібен перезапуск)",
|
||||||
|
|||||||
26
apps/client/src/types-fancytree.d.ts
vendored
26
apps/client/src/types-fancytree.d.ts
vendored
@@ -215,6 +215,30 @@ declare namespace Fancytree {
|
|||||||
enableUpdate(enabled: boolean): void;
|
enableUpdate(enabled: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FancytreeNodeData {
|
||||||
|
noteId: string;
|
||||||
|
parentNoteId: string;
|
||||||
|
branchId: string;
|
||||||
|
isProtected: boolean;
|
||||||
|
noteType: NoteType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FancytreeNewNode extends FancytreeNodeData {
|
||||||
|
title: string;
|
||||||
|
extraClasses: string;
|
||||||
|
icon: string;
|
||||||
|
refKey: string;
|
||||||
|
/** True if this node is loaded on demand, i.e. on first expansion. */
|
||||||
|
lazy: boolean;
|
||||||
|
/** Folder nodes have different default icons and click behavior. Note: Also non-folders may have children. */
|
||||||
|
folder: boolean;
|
||||||
|
/** Use isExpanded(), setExpanded() to access this property. */
|
||||||
|
expanded: boolean;
|
||||||
|
/** Node id (must be unique inside the tree) */
|
||||||
|
key: string;
|
||||||
|
children?: FancytreeNewNode[];
|
||||||
|
}
|
||||||
|
|
||||||
/** A FancytreeNode represents the hierarchical data model and operations. */
|
/** A FancytreeNode represents the hierarchical data model and operations. */
|
||||||
interface FancytreeNode {
|
interface FancytreeNode {
|
||||||
// #region Properties
|
// #region Properties
|
||||||
@@ -227,7 +251,7 @@ declare namespace Fancytree {
|
|||||||
/** Display name (may contain HTML) */
|
/** Display name (may contain HTML) */
|
||||||
title: string;
|
title: string;
|
||||||
/** Contains all extra data that was passed on node creation */
|
/** Contains all extra data that was passed on node creation */
|
||||||
data: any;
|
data: FancytreeNodeData;
|
||||||
/** Array of child nodes. For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array to define a node that has no children. */
|
/** Array of child nodes. For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array to define a node that has no children. */
|
||||||
children: FancytreeNode[];
|
children: FancytreeNode[];
|
||||||
/** Use isExpanded(), setExpanded() to access this property. */
|
/** Use isExpanded(), setExpanded() to access this property. */
|
||||||
|
|||||||
@@ -23,6 +23,24 @@ export class CssVarReader {
|
|||||||
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
|
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
asBoolean(defaultValue?: boolean) {
|
||||||
|
let value = this.value.toLocaleLowerCase().trim();
|
||||||
|
let result: boolean | undefined;
|
||||||
|
|
||||||
|
switch (value) {
|
||||||
|
case "true":
|
||||||
|
case "1":
|
||||||
|
result = true;
|
||||||
|
break;
|
||||||
|
case "false":
|
||||||
|
case "0":
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (result !== undefined) ? result : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
|
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
|
||||||
let result: T[keyof T] | undefined;
|
let result: T[keyof T] | undefined;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
.floating-buttons-children,
|
.floating-buttons-children,
|
||||||
.show-floating-buttons {
|
.show-floating-buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--floating-buttons-vert-offset, 10px);
|
top: var(--floating-buttons-vert-offset, 14px);
|
||||||
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
|
inset-inline-end: var(--floating-buttons-horiz-offset, 10px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import "./FloatingButtons.css";
|
import "./FloatingButtons.css";
|
||||||
import { useNoteContext, useNoteLabel, useNoteLabelBoolean } from "./react/hooks";
|
import { useNoteContext, useNoteLabel, useNoteLabelBoolean, useTriliumEvent } from "./react/hooks";
|
||||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { ParentComponent } from "./react/react_utils";
|
import { ParentComponent } from "./react/react_utils";
|
||||||
import { EventData, EventNames } from "../components/app_context";
|
import { EventData, EventNames } from "../components/app_context";
|
||||||
@@ -20,6 +20,7 @@ interface FloatingButtonsProps {
|
|||||||
* properly handle rounded corners, as defined by the --border-radius CSS variable.
|
* properly handle rounded corners, as defined by the --border-radius CSS variable.
|
||||||
*/
|
*/
|
||||||
export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
||||||
|
const [ top, setTop ] = useState(0);
|
||||||
const { note, noteContext } = useNoteContext();
|
const { note, noteContext } = useNoteContext();
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||||
@@ -47,8 +48,14 @@ export default function FloatingButtons({ items }: FloatingButtonsProps) {
|
|||||||
const [ visible, setVisible ] = useState(true);
|
const [ visible, setVisible ] = useState(true);
|
||||||
useEffect(() => setVisible(true), [ note ]);
|
useEffect(() => setVisible(true), [ note ]);
|
||||||
|
|
||||||
|
useTriliumEvent("contentSafeMarginChanged", (e) => {
|
||||||
|
if (e.noteContext === noteContext) {
|
||||||
|
setTop(e.top);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="floating-buttons no-print">
|
<div className="floating-buttons no-print" style={{top}}>
|
||||||
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
<div className={`floating-buttons-children ${!visible ? "temporarily-hidden" : ""}`}>
|
||||||
{context && items.map((Component) => (
|
{context && items.map((Component) => (
|
||||||
<Component {...context} />
|
<Component {...context} />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Component from "../components/component";
|
|||||||
import NoteContext from "../components/note_context";
|
import NoteContext from "../components/note_context";
|
||||||
import FNote from "../entities/fnote";
|
import FNote from "../entities/fnote";
|
||||||
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
import ActionButton, { ActionButtonProps } from "./react/ActionButton";
|
||||||
import { useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks";
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils";
|
||||||
import server from "../services/server";
|
import server from "../services/server";
|
||||||
@@ -13,8 +13,6 @@ import toast from "../services/toast";
|
|||||||
import { t } from "../services/i18n";
|
import { t } from "../services/i18n";
|
||||||
import { copyImageReferenceToClipboard } from "../services/image";
|
import { copyImageReferenceToClipboard } from "../services/image";
|
||||||
import tree from "../services/tree";
|
import tree from "../services/tree";
|
||||||
import protected_session_holder from "../services/protected_session_holder";
|
|
||||||
import options from "../services/options";
|
|
||||||
import { getHelpUrlForNote } from "../services/in_app_help";
|
import { getHelpUrlForNote } from "../services/in_app_help";
|
||||||
import froca from "../services/froca";
|
import froca from "../services/froca";
|
||||||
import NoteLink from "./react/NoteLink";
|
import NoteLink from "./react/NoteLink";
|
||||||
@@ -101,48 +99,26 @@ function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingBut
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditButton({ note, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
function EditButton({ note, noteContext }: FloatingButtonContext) {
|
||||||
const [ animationClass, setAnimationClass ] = useState("");
|
const [animationClass, setAnimationClass] = useState("");
|
||||||
const [ isEnabled, setIsEnabled ] = useState(false);
|
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||||
|
|
||||||
|
const isReadOnlyInfoBarDismissed = false; // TODO
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
noteContext.isReadOnly().then(isReadOnly => {
|
if (isReadOnly) {
|
||||||
setIsEnabled(
|
|
||||||
isDefaultViewMode
|
|
||||||
&& (!note.isProtected || protected_session_holder.isProtectedSessionAvailable())
|
|
||||||
&& !options.is("databaseReadonly")
|
|
||||||
&& isReadOnly
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [ note ]);
|
|
||||||
|
|
||||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
|
||||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
|
||||||
setIsEnabled(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// make the edit button stand out on the first display, otherwise
|
|
||||||
// it's difficult to notice that the note is readonly
|
|
||||||
useEffect(() => {
|
|
||||||
if (isEnabled) {
|
|
||||||
setAnimationClass("bx-tada bx-lg");
|
setAnimationClass("bx-tada bx-lg");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setAnimationClass("");
|
setAnimationClass("");
|
||||||
}, 1700);
|
}, 1700);
|
||||||
}
|
}
|
||||||
}, [ isEnabled ]);
|
}, [ isReadOnly ]);
|
||||||
|
|
||||||
return isEnabled && <FloatingButton
|
return !!isReadOnly && isReadOnlyInfoBarDismissed && <FloatingButton
|
||||||
text={t("edit_button.edit_this_note")}
|
text={t("edit_button.edit_this_note")}
|
||||||
icon="bx bx-pencil"
|
icon="bx bx-pencil"
|
||||||
className={animationClass}
|
className={animationClass}
|
||||||
onClick={() => {
|
onClick={() => enableEditing()}
|
||||||
if (noteContext.viewScope) {
|
|
||||||
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
|
||||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", { noteContext });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
apps/client/src/widgets/ReadOnlyNoteInfoBar.css
Normal file
19
apps/client/src/widgets/ReadOnlyNoteInfoBar.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
body.zen div.read-only-note-info-bar-widget {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: var(--max-content-width);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: unset;
|
||||||
|
margin: 0 auto 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-only-note-info-bar-widget-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root div.read-only-note-info-bar-widget button {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
36
apps/client/src/widgets/ReadOnlyNoteInfoBar.tsx
Normal file
36
apps/client/src/widgets/ReadOnlyNoteInfoBar.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import "./ReadOnlyNoteInfoBar.css";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
|
||||||
|
import Button from "./react/Button";
|
||||||
|
import InfoBar from "./react/InfoBar";
|
||||||
|
|
||||||
|
export default function ReadOnlyNoteInfoBar(props: {}) {
|
||||||
|
const {note, noteContext} = useNoteContext();
|
||||||
|
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||||
|
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||||
|
|
||||||
|
return <InfoBar className="read-only-note-info-bar-widget"
|
||||||
|
type={(isExplicitReadOnly ? "subtle" : "prominent")}
|
||||||
|
style={{display: (!isReadOnly) ? "none" : undefined}}>
|
||||||
|
|
||||||
|
<div class="read-only-note-info-bar-widget-content">
|
||||||
|
{(isExplicitReadOnly) ? (
|
||||||
|
<div>{t("read-only-info.read-only-note")}</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{t("read-only-info.auto-read-only-note")}
|
||||||
|
|
||||||
|
<a class="tn-link"
|
||||||
|
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode">
|
||||||
|
|
||||||
|
{t("read-only-info.auto-read-only-learn-more")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button text={t("read-only-info.edit-note")}
|
||||||
|
icon="bx-pencil" onClick={() => enableEditing()} />
|
||||||
|
</div>
|
||||||
|
</InfoBar>
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
.note-list-widget {
|
.note-list-widget {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
contain: none !important;
|
contain: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.prefers-centered-content .note-list-widget:not(.full-height) {
|
||||||
|
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.note-list-widget .note-list {
|
.note-list-widget .note-list {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
32
apps/client/src/widgets/collections/board/data.spec.ts
Normal file
32
apps/client/src/widgets/collections/board/data.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { it, describe, expect } from "vitest";
|
||||||
|
import { buildNote } from "../../../test/easy-froca";
|
||||||
|
import { getBoardData } from "./data";
|
||||||
|
import FBranch from "../../../entities/fbranch";
|
||||||
|
import froca from "../../../services/froca";
|
||||||
|
|
||||||
|
describe("Board data", () => {
|
||||||
|
it("deduplicates cloned notes", async () => {
|
||||||
|
const parentNote = buildNote({
|
||||||
|
title: "Board",
|
||||||
|
"#collection": "",
|
||||||
|
"#viewType": "board",
|
||||||
|
children: [
|
||||||
|
{ id: "note1", title: "First note", "#status": "To Do" },
|
||||||
|
{ id: "note2", title: "Second note", "#status": "In progress" },
|
||||||
|
{ id: "note3", title: "Third note", "#status": "Done" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const branch = new FBranch(froca, {
|
||||||
|
branchId: "note1_note2",
|
||||||
|
notePosition: 10,
|
||||||
|
fromSearchNote: false,
|
||||||
|
noteId: "note2",
|
||||||
|
parentNoteId: "note1"
|
||||||
|
});
|
||||||
|
froca.branches["note1_note2"] = branch;
|
||||||
|
froca.getNoteFromCache("note1").addChild("note2", "note1_note2", false);
|
||||||
|
const data = await getBoardData(parentNote, "status", {}, false);
|
||||||
|
const noteIds = Array.from(data.byColumn.values()).flat().map(item => item.note.noteId);
|
||||||
|
expect(noteIds.length).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
|||||||
const byColumn: ColumnMap = new Map();
|
const byColumn: ColumnMap = new Map();
|
||||||
|
|
||||||
// First, scan all notes to find what columns actually exist
|
// First, scan all notes to find what columns actually exist
|
||||||
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived);
|
await recursiveGroupBy(parentNote.getChildBranches(), byColumn, groupByColumn, includeArchived, new Set<string>());
|
||||||
|
|
||||||
// Get all columns that exist in the notes
|
// Get all columns that exist in the notes
|
||||||
const columnsFromNotes = [...byColumn.keys()];
|
const columnsFromNotes = [...byColumn.keys()];
|
||||||
@@ -61,26 +61,28 @@ export async function getBoardData(parentNote: FNote, groupByColumn: string, per
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean) {
|
async function recursiveGroupBy(branches: FBranch[], byColumn: ColumnMap, groupByColumn: string, includeArchived: boolean, seenNoteIds: Set<string>) {
|
||||||
for (const branch of branches) {
|
for (const branch of branches) {
|
||||||
const note = await branch.getNote();
|
const note = await branch.getNote();
|
||||||
if (!note || (!includeArchived && note.isArchived)) continue;
|
if (!note || (!includeArchived && note.isArchived)) continue;
|
||||||
|
|
||||||
if (note.type !== "search" && note.hasChildren()) {
|
if (note.type !== "search" && note.hasChildren()) {
|
||||||
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived);
|
await recursiveGroupBy(note.getChildBranches(), byColumn, groupByColumn, includeArchived, seenNoteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = note.getLabelValue(groupByColumn);
|
const group = note.getLabelValue(groupByColumn);
|
||||||
if (!group) {
|
if (!group || seenNoteIds.has(note.noteId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!byColumn.has(group)) {
|
if (!byColumn.has(group)) {
|
||||||
byColumn.set(group, []);
|
byColumn.set(group, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
byColumn.get(group)!.push({
|
byColumn.get(group)!.push({
|
||||||
branch,
|
branch,
|
||||||
note
|
note
|
||||||
});
|
});
|
||||||
|
seenNoteIds.add(note.noteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,8 +58,6 @@ export async function changeEvent(note: FNote, { startDate, endDate, startTime,
|
|||||||
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
|
startAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:startTime").shift()?.value||"startTime";
|
||||||
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
|
endAttribute = note.getAttributes("label").filter(attr => attr.name == "calendar:endTime").shift()?.value||"endTime";
|
||||||
|
|
||||||
if (startTime && endTime) {
|
setAttribute(note, "label", startAttribute, startTime);
|
||||||
setAttribute(note, "label", startAttribute, startTime);
|
setAttribute(note, "label", endAttribute, endTime);
|
||||||
setAttribute(note, "label", endAttribute, endTime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
28
apps/client/src/widgets/collections/calendar/context_menu.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
import contextMenu, { ContextMenuEvent } from "../../../menus/context_menu";
|
||||||
|
import link_context_menu from "../../../menus/link_context_menu";
|
||||||
|
import branches from "../../../services/branches";
|
||||||
|
import { t } from "../../../services/i18n";
|
||||||
|
|
||||||
|
export function openCalendarContextMenu(e: ContextMenuEvent, noteId: string, parentNote: FNote) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
contextMenu.show({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
items: [
|
||||||
|
...link_context_menu.getItems(),
|
||||||
|
{ kind: "separator" },
|
||||||
|
{
|
||||||
|
title: t("calendar_view.delete_note"),
|
||||||
|
uiIcon: "bx bx-trash",
|
||||||
|
handler: async () => {
|
||||||
|
const branchId = parentNote.childToBranch[noteId];
|
||||||
|
await branches.deleteNotes([ branchId ], false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: ({ command }) => link_context_menu.handleLinkContextMenuItem(command, noteId),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
.calendar-view {
|
.calendar-view {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
outline: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body.desktop:not(.zen) .calendar-view .calendar-header {
|
body.desktop:not(.zen) .calendar-view .calendar-header {
|
||||||
|
padding-block-start: 4px;
|
||||||
padding-inline-end: 5em;
|
padding-inline-end: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import Button, { ButtonGroup } from "../../react/Button";
|
|||||||
import ActionButton from "../../react/ActionButton";
|
import ActionButton from "../../react/ActionButton";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
import TouchBar, { TouchBarButton, TouchBarLabel, TouchBarSegmentedControl, TouchBarSpacer } from "../../react/TouchBar";
|
||||||
|
import { openCalendarContextMenu } from "./context_menu";
|
||||||
|
|
||||||
interface CalendarViewData {
|
interface CalendarViewData {
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
|
|||||||
const plugins = usePlugins(isEditable, isCalendarRoot);
|
const plugins = usePlugins(isEditable, isCalendarRoot);
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
const { eventDidMount } = useEventDisplayCustomization();
|
const { eventDidMount } = useEventDisplayCustomization(note);
|
||||||
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
const editingProps = useEditing(note, isEditable, isCalendarRoot);
|
||||||
|
|
||||||
// React to changes.
|
// React to changes.
|
||||||
@@ -196,11 +197,11 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useLocale() {
|
function useLocale() {
|
||||||
const [ locale ] = useTriliumOption("locale");
|
const [ formattingLocale ] = useTriliumOption("formattingLocale");
|
||||||
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
|
const [ calendarLocale, setCalendarLocale ] = useState<LocaleInput>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const correspondingLocale = LOCALE_MAPPINGS[locale];
|
const correspondingLocale = LOCALE_MAPPINGS[formattingLocale];
|
||||||
if (correspondingLocale) {
|
if (correspondingLocale) {
|
||||||
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
|
correspondingLocale().then((locale) => setCalendarLocale(locale.default));
|
||||||
} else {
|
} else {
|
||||||
@@ -253,7 +254,7 @@ function useEditing(note: FNote, isEditable: boolean, isCalendarRoot: boolean) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function useEventDisplayCustomization() {
|
function useEventDisplayCustomization(parentNote: FNote) {
|
||||||
const eventDidMount = useCallback((e: EventMountArg) => {
|
const eventDidMount = useCallback((e: EventMountArg) => {
|
||||||
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
const { iconClass, promotedAttributes } = e.event.extendedProps;
|
||||||
|
|
||||||
@@ -302,6 +303,11 @@ function useEventDisplayCustomization() {
|
|||||||
}
|
}
|
||||||
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
$(mainContainer ?? e.el).append($(promotedAttributesHtml));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.el.addEventListener("contextmenu", (contextMenuEvent) => {
|
||||||
|
const noteId = e.event.extendedProps.noteId;
|
||||||
|
openCalendarContextMenu(contextMenuEvent, noteId, parentNote);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
return { eventDidMount };
|
return { eventDidMount };
|
||||||
}
|
}
|
||||||
|
|||||||
62
apps/client/src/widgets/containers/content-header.ts
Normal file
62
apps/client/src/widgets/containers/content-header.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { EventData } from "../../components/app_context";
|
||||||
|
import BasicWidget from "../basic_widget";
|
||||||
|
import Container from "./container";
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
|
||||||
|
export default class ContentHeader extends Container<BasicWidget> {
|
||||||
|
|
||||||
|
noteContext?: NoteContext;
|
||||||
|
thisElement?: HTMLElement;
|
||||||
|
parentElement?: HTMLElement;
|
||||||
|
resizeObserver: ResizeObserver;
|
||||||
|
currentHeight: number = 0;
|
||||||
|
currentSafeMargin: number = NaN;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.css("contain", "unset");
|
||||||
|
this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
|
||||||
|
this.noteContext = noteContext;
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.parentElement = this.parent?.$widget.get(0);
|
||||||
|
if (!this.parentElement) {
|
||||||
|
console.warn("No parent set for <ContentHeader>.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.thisElement = this.$widget.get(0)!;
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.thisElement);
|
||||||
|
this.parentElement.addEventListener("scroll", this.updateSafeMargin.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSafeMargin() {
|
||||||
|
const newSafeMargin = Math.max(this.currentHeight - this.parentElement!.scrollTop, 0);
|
||||||
|
|
||||||
|
if (newSafeMargin !== this.currentSafeMargin) {
|
||||||
|
this.currentSafeMargin = newSafeMargin;
|
||||||
|
|
||||||
|
this.triggerEvent("contentSafeMarginChanged", {
|
||||||
|
top: newSafeMargin,
|
||||||
|
noteContext: this.noteContext!
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize(entries: ResizeObserverEntry[]) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.target === this.thisElement) {
|
||||||
|
this.currentHeight = entry.contentRect.height;
|
||||||
|
this.updateSafeMargin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { EventData } from "../../components/app_context.js";
|
import { EventData } from "../../components/app_context.js";
|
||||||
|
import { LOCALES } from "@triliumnext/commons";
|
||||||
|
import { readCssVar } from "../../utils/css-var.js";
|
||||||
import FlexContainer from "./flex_container.js";
|
import FlexContainer from "./flex_container.js";
|
||||||
import options from "../../services/options.js";
|
import options from "../../services/options.js";
|
||||||
import type BasicWidget from "../basic_widget.js";
|
import type BasicWidget from "../basic_widget.js";
|
||||||
import utils from "../../services/utils.js";
|
import utils from "../../services/utils.js";
|
||||||
import { LOCALES } from "@triliumnext/commons";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||||
@@ -30,9 +31,11 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#setMotion(options.is("motionEnabled"));
|
this.#setMaxContentWidth();
|
||||||
this.#setShadows(options.is("shadowsEnabled"));
|
this.#setMotion();
|
||||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
this.#setShadows();
|
||||||
|
this.#setBackdropEffects();
|
||||||
|
this.#setThemeCapabilities();
|
||||||
this.#setLocaleAndDirection(options.get("locale"));
|
this.#setLocaleAndDirection(options.get("locale"));
|
||||||
|
|
||||||
return super.render();
|
return super.render();
|
||||||
@@ -40,15 +43,21 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
|
|
||||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
if (loadResults.isOptionReloaded("motionEnabled")) {
|
if (loadResults.isOptionReloaded("motionEnabled")) {
|
||||||
this.#setMotion(options.is("motionEnabled"));
|
this.#setMotion();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResults.isOptionReloaded("shadowsEnabled")) {
|
if (loadResults.isOptionReloaded("shadowsEnabled")) {
|
||||||
this.#setShadows(options.is("shadowsEnabled"));
|
this.#setShadows();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
|
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
|
||||||
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
this.#setBackdropEffects();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadResults.isOptionReloaded("maxContentWidth")
|
||||||
|
|| loadResults.isOptionReloaded("centerContent")) {
|
||||||
|
|
||||||
|
this.#setMaxContentWidth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,19 +67,38 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setMotion(enabled: boolean) {
|
#setMaxContentWidth() {
|
||||||
|
const width = Math.max(options.getInt("maxContentWidth") || 0, 640);
|
||||||
|
document.body.style.setProperty("--preferred-max-content-width", `${width}px`);
|
||||||
|
|
||||||
|
document.body.classList.toggle("prefers-centered-content", options.is("centerContent"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#setMotion() {
|
||||||
|
const enabled = options.is("motionEnabled");
|
||||||
document.body.classList.toggle("motion-disabled", !enabled);
|
document.body.classList.toggle("motion-disabled", !enabled);
|
||||||
jQuery.fx.off = !enabled;
|
jQuery.fx.off = !enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
#setShadows(enabled: boolean) {
|
#setShadows() {
|
||||||
|
const enabled = options.is("shadowsEnabled");
|
||||||
document.body.classList.toggle("shadows-disabled", !enabled);
|
document.body.classList.toggle("shadows-disabled", !enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
#setBackdropEffects(enabled: boolean) {
|
#setBackdropEffects() {
|
||||||
|
const enabled = options.is("backdropEffectsEnabled");
|
||||||
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setThemeCapabilities() {
|
||||||
|
// Supports background effects
|
||||||
|
|
||||||
|
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
|
||||||
|
.asBoolean(false);
|
||||||
|
|
||||||
|
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
|
||||||
|
}
|
||||||
|
|
||||||
#setLocaleAndDirection(locale: string) {
|
#setLocaleAndDirection(locale: string) {
|
||||||
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
const correspondingLocale = LOCALES.find(l => l.id === locale);
|
||||||
document.body.lang = locale;
|
document.body.lang = locale;
|
||||||
|
|||||||
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
13
apps/client/src/widgets/dialogs/branch_prefix.css
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.branch-prefix-dialog .branch-prefix-notes-list {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-notes-list ul {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-prefix-dialog .branch-prefix-current {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
@@ -10,53 +10,86 @@ import Button from "../react/Button.jsx";
|
|||||||
import FormGroup from "../react/FormGroup.js";
|
import FormGroup from "../react/FormGroup.js";
|
||||||
import { useTriliumEvent } from "../react/hooks.jsx";
|
import { useTriliumEvent } from "../react/hooks.jsx";
|
||||||
import FBranch from "../../entities/fbranch.js";
|
import FBranch from "../../entities/fbranch.js";
|
||||||
|
import type { ContextMenuCommandData } from "../../components/app_context.js";
|
||||||
|
import "./branch_prefix.css";
|
||||||
|
|
||||||
|
// Virtual branches (e.g., from search results) start with this prefix
|
||||||
|
const VIRTUAL_BRANCH_PREFIX = "virt-";
|
||||||
|
|
||||||
export default function BranchPrefixDialog() {
|
export default function BranchPrefixDialog() {
|
||||||
const [ shown, setShown ] = useState(false);
|
const [ shown, setShown ] = useState(false);
|
||||||
const [ branch, setBranch ] = useState<FBranch>();
|
const [ branches, setBranches ] = useState<FBranch[]>([]);
|
||||||
const [ prefix, setPrefix ] = useState("");
|
const [ prefix, setPrefix ] = useState("");
|
||||||
const branchInput = useRef<HTMLInputElement>(null);
|
const branchInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useTriliumEvent("editBranchPrefix", async () => {
|
useTriliumEvent("editBranchPrefix", async (data?: ContextMenuCommandData) => {
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
let branchIds: string[] = [];
|
||||||
if (!notePath) {
|
|
||||||
|
if (data?.selectedOrActiveBranchIds && data.selectedOrActiveBranchIds.length > 0) {
|
||||||
|
// Multi-select mode from tree context menu
|
||||||
|
branchIds = data.selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith(VIRTUAL_BRANCH_PREFIX));
|
||||||
|
} else {
|
||||||
|
// Single branch mode from keyboard shortcut or when no selection
|
||||||
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
if (!notePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
||||||
|
|
||||||
|
if (!noteId || !parentNoteId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchId = await froca.getBranchId(parentNoteId, noteId);
|
||||||
|
if (!branchId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentNote = await froca.getNote(parentNoteId);
|
||||||
|
if (!parentNote || parentNote.type === "search") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
branchIds = [branchId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (branchIds.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { noteId, parentNoteId } = tree.getNoteIdAndParentIdFromUrl(notePath);
|
const newBranches = branchIds
|
||||||
|
.map(id => froca.getBranch(id))
|
||||||
|
.filter((branch): branch is FBranch => branch !== null);
|
||||||
|
|
||||||
if (!noteId || !parentNoteId) {
|
if (newBranches.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBranchId = await froca.getBranchId(parentNoteId, noteId);
|
setBranches(newBranches);
|
||||||
if (!newBranchId) {
|
// Use the prefix of the first branch as the initial value
|
||||||
return;
|
setPrefix(newBranches[0]?.prefix ?? "");
|
||||||
}
|
|
||||||
const parentNote = await froca.getNote(parentNoteId);
|
|
||||||
if (!parentNote || parentNote.type === "search") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newBranch = froca.getBranch(newBranchId);
|
|
||||||
setBranch(newBranch);
|
|
||||||
setPrefix(newBranch?.prefix ?? "");
|
|
||||||
setShown(true);
|
setShown(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!branch) {
|
if (branches.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
savePrefix(branch.branchId, prefix);
|
if (branches.length === 1) {
|
||||||
|
await savePrefix(branches[0].branchId, prefix);
|
||||||
|
} else {
|
||||||
|
await savePrefixBatch(branches.map(b => b.branchId), prefix);
|
||||||
|
}
|
||||||
setShown(false);
|
setShown(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSingleBranch = branches.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className="branch-prefix-dialog"
|
className="branch-prefix-dialog"
|
||||||
title={t("branch_prefix.edit_branch_prefix")}
|
title={isSingleBranch ? t("branch_prefix.edit_branch_prefix") : t("branch_prefix.edit_branch_prefix_multiple", { count: branches.length })}
|
||||||
size="lg"
|
size="lg"
|
||||||
onShown={() => branchInput.current?.focus()}
|
onShown={() => branchInput.current?.focus()}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
@@ -69,9 +102,27 @@ export default function BranchPrefixDialog() {
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
<input class="branch-prefix-input form-control" value={prefix} ref={branchInput}
|
||||||
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
onChange={(e) => setPrefix((e.target as HTMLInputElement).value)} />
|
||||||
<div class="branch-prefix-note-title input-group-text"> - {branch && branch.getNoteFromCache().title}</div>
|
{isSingleBranch && branches[0] && (
|
||||||
|
<div class="branch-prefix-note-title input-group-text"> - {branches[0].getNoteFromCache().title}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{!isSingleBranch && (
|
||||||
|
<div className="branch-prefix-notes-list">
|
||||||
|
<strong>{t("branch_prefix.affected_branches", { count: branches.length })}</strong>
|
||||||
|
<ul>
|
||||||
|
{branches.map((branch) => {
|
||||||
|
const note = branch.getNoteFromCache();
|
||||||
|
return (
|
||||||
|
<li key={branch.branchId}>
|
||||||
|
{branch.prefix && <span className="branch-prefix-current">{branch.prefix} - </span>}
|
||||||
|
{note.title}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -80,3 +131,8 @@ async function savePrefix(branchId: string, prefix: string) {
|
|||||||
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
await server.put(`branches/${branchId}/set-prefix`, { prefix: prefix });
|
||||||
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
toast.showMessage(t("branch_prefix.branch_prefix_saved"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function savePrefixBatch(branchIds: string[], prefix: string) {
|
||||||
|
await server.put("branches/set-prefix-batch", { branchIds, prefix });
|
||||||
|
toast.showMessage(t("branch_prefix.branch_prefix_saved_multiple", { count: branchIds.length }));
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import TypeWidget from "../type_widgets/type_widget.js";
|
|||||||
const TPL = /*html*/`\
|
const TPL = /*html*/`\
|
||||||
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
<div class="popup-editor-dialog modal fade mx-auto" tabindex="-1" role="dialog">
|
||||||
<style>
|
<style>
|
||||||
|
/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */
|
||||||
|
body.popup-editor-open > .modal-backdrop { z-index: 998; }
|
||||||
|
body.popup-editor-open .popup-editor-dialog { z-index: 999; }
|
||||||
|
body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
|
||||||
|
|
||||||
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
body.desktop .modal.popup-editor-dialog .modal-dialog {
|
||||||
max-width: 75vw;
|
max-width: 75vw;
|
||||||
}
|
}
|
||||||
@@ -57,17 +62,19 @@ const TPL = /*html*/`\
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="quick-edit-dialog-wrapper">
|
||||||
<div class="modal-content">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-header">
|
<div class="modal-content">
|
||||||
<div class="modal-title">
|
<div class="modal-header">
|
||||||
<!-- This is where the first child will be injected -->
|
<div class="modal-title">
|
||||||
|
<!-- This is where the first child will be injected -->
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<!-- This is where all but the first child will be injected. -->
|
<!-- This is where all but the first child will be injected. -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,6 +86,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
|||||||
private noteContext: NoteContext;
|
private noteContext: NoteContext;
|
||||||
private $modalHeader!: JQuery<HTMLElement>;
|
private $modalHeader!: JQuery<HTMLElement>;
|
||||||
private $modalBody!: JQuery<HTMLElement>;
|
private $modalBody!: JQuery<HTMLElement>;
|
||||||
|
private $wrapper!: JQuery<HTMLDivElement>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -93,6 +101,7 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
|||||||
const $newWidget = $(TPL);
|
const $newWidget = $(TPL);
|
||||||
this.$modalHeader = $newWidget.find(".modal-title");
|
this.$modalHeader = $newWidget.find(".modal-title");
|
||||||
this.$modalBody = $newWidget.find(".modal-body");
|
this.$modalBody = $newWidget.find(".modal-body");
|
||||||
|
this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper");
|
||||||
|
|
||||||
const children = this.$widget.children();
|
const children = this.$widget.children();
|
||||||
this.$modalHeader.append(children[0]);
|
this.$modalHeader.append(children[0]);
|
||||||
@@ -112,17 +121,27 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const colorClass = this.noteContext.note?.getColorClass();
|
||||||
|
const wrapperElement = this.$wrapper.get(0)!;
|
||||||
|
|
||||||
|
if (colorClass) {
|
||||||
|
wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass;
|
||||||
|
} else {
|
||||||
|
wrapperElement.className = "quick-edit-dialog-wrapper";
|
||||||
|
}
|
||||||
|
|
||||||
|
const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue");
|
||||||
|
if (customHue) {
|
||||||
|
/* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */
|
||||||
|
wrapperElement.classList.add("tinted-quick-edit-dialog");
|
||||||
|
}
|
||||||
|
|
||||||
const activeEl = document.activeElement;
|
const activeEl = document.activeElement;
|
||||||
if (activeEl && "blur" in activeEl) {
|
if (activeEl && "blur" in activeEl) {
|
||||||
(activeEl as HTMLElement).blur();
|
(activeEl as HTMLElement).blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
$dialog.on("shown.bs.modal", async () => {
|
$dialog.on("shown.bs.modal", async () => {
|
||||||
// Reduce the z-index of modals so that ckeditor popups are properly shown on top of it.
|
|
||||||
// The backdrop instance is not shared so it's OK to make a one-off modification.
|
|
||||||
$("body > .modal-backdrop").css("z-index", "998");
|
|
||||||
$dialog.css("z-index", "999");
|
|
||||||
|
|
||||||
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
|
await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext });
|
||||||
this.setVisibility(true);
|
this.setVisibility(true);
|
||||||
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
|
await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId });
|
||||||
@@ -143,9 +162,12 @@ export default class PopupEditorDialog extends Container<BasicWidget> {
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
$bodyItems.fadeIn();
|
$bodyItems.fadeIn();
|
||||||
this.$modalHeader.children().show();
|
this.$modalHeader.children().show();
|
||||||
|
document.body.classList.add("popup-editor-open");
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
$bodyItems.hide();
|
$bodyItems.hide();
|
||||||
this.$modalHeader.children().hide();
|
this.$modalHeader.children().hide();
|
||||||
|
document.body.classList.remove("popup-editor-open");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ const TPL = /*html*/`
|
|||||||
<div class="note-detail">
|
<div class="note-detail">
|
||||||
<style>
|
<style>
|
||||||
.note-detail {
|
.note-detail {
|
||||||
|
max-width: var(--max-content-width); /* Inherited from .note-split */
|
||||||
font-family: var(--detail-font-family);
|
font-family: var(--detail-font-family);
|
||||||
font-size: var(--detail-font-size);
|
font-size: var(--detail-font-size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-detail.full-height {
|
body.prefers-centered-content .note-detail {
|
||||||
height: 100%;
|
/* Horizontally center the widget in its parent when the "Keep content centered" option is on */
|
||||||
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -173,14 +173,6 @@ interface ExpandedSubtreeResponse {
|
|||||||
branchIds: string[];
|
branchIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Node extends Fancytree.NodeData {
|
|
||||||
noteId: string;
|
|
||||||
parentNoteId: string;
|
|
||||||
branchId: string;
|
|
||||||
isProtected: boolean;
|
|
||||||
noteType: NoteType;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RefreshContext {
|
interface RefreshContext {
|
||||||
noteIdsToUpdate: Set<string>;
|
noteIdsToUpdate: Set<string>;
|
||||||
noteIdsToReload: Set<string>;
|
noteIdsToReload: Set<string>;
|
||||||
@@ -769,7 +761,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
prepareChildren(parentNote: FNote) {
|
prepareChildren(parentNote: FNote) {
|
||||||
utils.assertArguments(parentNote);
|
utils.assertArguments(parentNote);
|
||||||
|
|
||||||
const noteList: Node[] = [];
|
const noteList: Fancytree.FancytreeNewNode[] = [];
|
||||||
|
|
||||||
const hideArchivedNotes = this.hideArchivedNotes;
|
const hideArchivedNotes = this.hideArchivedNotes;
|
||||||
|
|
||||||
@@ -837,7 +829,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
const isFolder = note.isFolder();
|
const isFolder = note.isFolder();
|
||||||
|
|
||||||
const node: Node = {
|
const node: Fancytree.FancytreeNewNode = {
|
||||||
noteId: note.noteId,
|
noteId: note.noteId,
|
||||||
parentNoteId: branch.parentNoteId,
|
parentNoteId: branch.parentNoteId,
|
||||||
branchId: branch.branchId,
|
branchId: branch.branchId,
|
||||||
@@ -849,7 +841,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
refKey: note.noteId,
|
refKey: note.noteId,
|
||||||
lazy: true,
|
lazy: true,
|
||||||
folder: isFolder,
|
folder: isFolder,
|
||||||
expanded: branch.isExpanded && note.type !== "search",
|
expanded: !!branch.isExpanded && note.type !== "search",
|
||||||
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
key: utils.randomString(12) // this should prevent some "duplicate key" errors
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -911,7 +903,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
return extraClasses.join(" ");
|
return extraClasses.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns {FancytreeNode[]} */
|
|
||||||
getSelectedNodes(stopOnParents = false) {
|
getSelectedNodes(stopOnParents = false) {
|
||||||
return this.tree.getSelectedNodes(stopOnParents);
|
return this.tree.getSelectedNodes(stopOnParents);
|
||||||
}
|
}
|
||||||
@@ -1532,7 +1523,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
// Automatically expand the hoisted note by default
|
// Automatically expand the hoisted note by default
|
||||||
const node = this.getActiveNode();
|
const node = this.getActiveNode();
|
||||||
if (node?.data.noteId === this.noteContext.hoistedNoteId){
|
if (node && node.data.noteId === this.noteContext.hoistedNoteId){
|
||||||
this.setExpanded(node.data.branchId, true);
|
this.setExpanded(node.data.branchId, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1591,6 +1582,20 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
|||||||
this.clearSelectedNodes();
|
this.clearSelectedNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async editBranchPrefixCommand({ node }: CommandListenerData<"editBranchPrefix">) {
|
||||||
|
const branchIds = this.getSelectedOrActiveBranchIds(node).filter((branchId) => !branchId.startsWith("virt-"));
|
||||||
|
|
||||||
|
if (!branchIds.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger the event with the selected branch IDs
|
||||||
|
appContext.triggerEvent("editBranchPrefix", {
|
||||||
|
selectedOrActiveBranchIds: branchIds,
|
||||||
|
node: node
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
canBeMovedUpOrDown(node: Fancytree.FancytreeNode) {
|
||||||
if (node.data.noteId === "root") {
|
if (node.data.noteId === "root") {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
|||||||
|
|
||||||
const note = this.noteContext?.note;
|
const note = this.noteContext?.note;
|
||||||
if (!note) {
|
if (!note) {
|
||||||
|
this.$widget.addClass("bgfx empty-note");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
|||||||
|
|
||||||
this.$widget.addClass(utils.getNoteTypeClass(note.type));
|
this.$widget.addClass(utils.getNoteTypeClass(note.type));
|
||||||
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
this.$widget.addClass(utils.getMimeTypeClass(note.mime));
|
||||||
|
this.$widget.toggleClass(["bgfx", "options"], note.isOptions());
|
||||||
this.$widget.toggleClass("protected", note.isProtected);
|
this.$widget.toggleClass("protected", note.isProtected);
|
||||||
|
|
||||||
const noteLanguage = note?.getLabelValue("language");
|
const noteLanguage = note?.getLabelValue("language");
|
||||||
@@ -70,7 +71,7 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#isFullWidthNote(note: FNote) {
|
#isFullWidthNote(note: FNote) {
|
||||||
if (["image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
if (["code", "image", "mermaid", "book", "render", "canvas", "webView", "mindMap"].includes(note.type)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,15 +27,16 @@ interface CKEditorOpts {
|
|||||||
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
|
onClick?: (e: MouseEvent, pos?: ModelPosition | null) => void;
|
||||||
onKeyDown?: (e: KeyboardEvent) => void;
|
onKeyDown?: (e: KeyboardEvent) => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
|
onInitialized?: (editorInstance: CKTextEditor) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, ...restProps }: CKEditorOpts) {
|
export default function CKEditor({ apiRef, currentValue, editor, config, disableNewlines, disableSpellcheck, onChange, onClick, onInitialized, ...restProps }: CKEditorOpts) {
|
||||||
const editorContainerRef = useRef<HTMLDivElement>(null);
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const textEditorRef = useRef<CKTextEditor>(null);
|
const textEditorRef = useRef<CKTextEditor>(null);
|
||||||
useImperativeHandle(apiRef, () => {
|
useImperativeHandle(apiRef, () => {
|
||||||
return {
|
return {
|
||||||
focus() {
|
focus() {
|
||||||
editorContainerRef.current?.focus();
|
textEditorRef.current?.editing.view.focus();
|
||||||
textEditorRef.current?.model.change((writer) => {
|
textEditorRef.current?.model.change((writer) => {
|
||||||
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
|
const documentRoot = textEditorRef.current?.editing.model.document.getRoot();
|
||||||
if (documentRoot) {
|
if (documentRoot) {
|
||||||
@@ -83,6 +84,8 @@ export default function CKEditor({ apiRef, currentValue, editor, config, disable
|
|||||||
if (currentValue) {
|
if (currentValue) {
|
||||||
textEditor.setData(currentValue);
|
textEditor.setData(currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInitialized?.(textEditor);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
23
apps/client/src/widgets/react/InfoBar.css
Normal file
23
apps/client/src/widgets/react/InfoBar.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.info-bar {
|
||||||
|
--link-color: currentColor;
|
||||||
|
|
||||||
|
margin-top: 4px;
|
||||||
|
contain: unset !important;
|
||||||
|
padding: 8px 20px;
|
||||||
|
color: var(--read-only-note-info-bar-color);
|
||||||
|
font-size: .9em;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bar-prominent {
|
||||||
|
background: var(--alert-bar-background, var(--accented-background-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bar-subtle {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
border-bottom: 1px solid var(--main-border-color);
|
||||||
|
margin-block: 0;
|
||||||
|
margin-inline: 10px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
}
|
||||||
19
apps/client/src/widgets/react/InfoBar.tsx
Normal file
19
apps/client/src/widgets/react/InfoBar.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import "./InfoBar.css";
|
||||||
|
import { ComponentChildren, CSSProperties } from "preact";
|
||||||
|
|
||||||
|
export type InfoBarParams = {
|
||||||
|
type: "prominent" | "subtle",
|
||||||
|
className: string;
|
||||||
|
style: CSSProperties
|
||||||
|
children: ComponentChildren;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InfoBar(props: InfoBarParams) {
|
||||||
|
return <div className={`info-bar ${props.className} info-bar-${props.type}`} style={props.style}>
|
||||||
|
{props?.children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoBar.defaultProps = {
|
||||||
|
type: "prominent"
|
||||||
|
} as InfoBarParams
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { CSSProperties } from "preact/compat";
|
||||||
import { CommandListenerData, EventData, EventNames } from "../../components/app_context";
|
import { DragData } from "../note_tree";
|
||||||
import { ParentComponent } from "./react_utils";
|
|
||||||
import SpacedUpdate from "../../services/spaced_update";
|
|
||||||
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
|
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
|
||||||
import options, { type OptionValue } from "../../services/options";
|
import { Inputs, MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
|
import { ParentComponent } from "./react_utils";
|
||||||
import NoteContext from "../../components/note_context";
|
|
||||||
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
|
||||||
import FNote from "../../entities/fnote";
|
|
||||||
import attributes from "../../services/attributes";
|
|
||||||
import FBlob from "../../entities/fblob";
|
|
||||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
|
||||||
import { RefObject, VNode } from "preact";
|
import { RefObject, VNode } from "preact";
|
||||||
import { Tooltip } from "bootstrap";
|
import { Tooltip } from "bootstrap";
|
||||||
import { CSSProperties } from "preact/compat";
|
import { ViewMode } from "../../services/link";
|
||||||
|
import appContext, { CommandListenerData, EventData, EventNames } from "../../components/app_context";
|
||||||
|
import attributes from "../../services/attributes";
|
||||||
|
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
||||||
|
import Component from "../../components/component";
|
||||||
|
import FBlob from "../../entities/fblob";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
import keyboard_actions from "../../services/keyboard_actions";
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
import Mark from "mark.js";
|
import Mark from "mark.js";
|
||||||
import { DragData } from "../note_tree";
|
import NoteContext from "../../components/note_context";
|
||||||
import Component from "../../components/component";
|
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||||
|
import options, { type OptionValue } from "../../services/options";
|
||||||
|
import protected_session_holder from "../../services/protected_session_holder";
|
||||||
|
import SpacedUpdate from "../../services/spaced_update";
|
||||||
import toast, { ToastOptions } from "../../services/toast";
|
import toast, { ToastOptions } from "../../services/toast";
|
||||||
import { ViewMode } from "../../services/link";
|
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
|
||||||
|
|
||||||
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
@@ -198,6 +199,7 @@ export function useNoteContext() {
|
|||||||
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
const [ notePath, setNotePath ] = useState<string | null | undefined>();
|
||||||
const [ note, setNote ] = useState<FNote | null | undefined>();
|
const [ note, setNote ] = useState<FNote | null | undefined>();
|
||||||
const [ , setViewMode ] = useState<ViewMode>();
|
const [ , setViewMode ] = useState<ViewMode>();
|
||||||
|
const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState<boolean | null | undefined>(noteContext?.viewScope?.isReadOnly);
|
||||||
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
const [ refreshCounter, setRefreshCounter ] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -217,6 +219,11 @@ export function useNoteContext() {
|
|||||||
setRefreshCounter(refreshCounter + 1);
|
setRefreshCounter(refreshCounter + 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => {
|
||||||
|
if (eventNoteContext.ntxId === noteContext?.ntxId) {
|
||||||
|
setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
const parentComponent = useContext(ParentComponent) as ReactWrappedWidget;
|
||||||
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`);
|
||||||
@@ -230,7 +237,8 @@ export function useNoteContext() {
|
|||||||
viewScope: noteContext?.viewScope,
|
viewScope: noteContext?.viewScope,
|
||||||
componentId: parentComponent.componentId,
|
componentId: parentComponent.componentId,
|
||||||
noteContext,
|
noteContext,
|
||||||
parentComponent
|
parentComponent,
|
||||||
|
isReadOnlyTemporarilyDisabled
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -701,3 +709,51 @@ export function useResizeObserver(ref: RefObject<HTMLElement>, callback: () => v
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [ callback, ref ]);
|
}, [ callback, ref ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the current note is in read-only mode, while an editing mode is available,
|
||||||
|
* and provides a way to switch to editing mode.
|
||||||
|
*/
|
||||||
|
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
|
||||||
|
const [isReadOnly, setIsReadOnly] = useState<boolean | undefined>(undefined);
|
||||||
|
|
||||||
|
const enableEditing = useCallback(() => {
|
||||||
|
if (noteContext?.viewScope) {
|
||||||
|
noteContext.viewScope.readOnlyTemporarilyDisabled = true;
|
||||||
|
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
||||||
|
}
|
||||||
|
}, [noteContext]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (note && noteContext) {
|
||||||
|
isNoteReadOnly(note, noteContext).then((readOnly) => {
|
||||||
|
setIsReadOnly(readOnly);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [note, noteContext]);
|
||||||
|
|
||||||
|
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||||
|
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||||
|
setIsReadOnly(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {isReadOnly, enableEditing};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||||
|
|
||||||
|
if (note.isProtected && !protected_session_holder.isProtectedSessionAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.is("databaseReadonly")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteContext.viewScope?.viewMode !== "default" || !await noteContext.isReadOnly()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function EditedNotesTab({ note }: TabContext) {
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}))}
|
}), " ")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||||
import appContext, { CommandNames } from "../../components/app_context";
|
|
||||||
import FNote from "../../entities/fnote"
|
|
||||||
import dialog from "../../services/dialog";
|
|
||||||
import { t } from "../../services/i18n"
|
|
||||||
import server from "../../services/server";
|
|
||||||
import toast from "../../services/toast";
|
|
||||||
import ws from "../../services/ws";
|
|
||||||
import ActionButton from "../react/ActionButton"
|
|
||||||
import Dropdown from "../react/Dropdown";
|
|
||||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
||||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
import { t } from "../../services/i18n"
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
import NoteContext from "../../components/note_context";
|
import { useIsNoteReadOnly } from "../react/hooks";
|
||||||
|
import ActionButton from "../react/ActionButton"
|
||||||
|
import appContext, { CommandNames } from "../../components/app_context";
|
||||||
import branches from "../../services/branches";
|
import branches from "../../services/branches";
|
||||||
|
import dialog from "../../services/dialog";
|
||||||
|
import Dropdown from "../react/Dropdown";
|
||||||
|
import FNote from "../../entities/fnote"
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
import server from "../../services/server";
|
||||||
|
import toast from "../../services/toast";
|
||||||
|
import ws from "../../services/ws";
|
||||||
|
|
||||||
interface NoteActionsProps {
|
interface NoteActionsProps {
|
||||||
note?: FNote;
|
note?: FNote;
|
||||||
@@ -52,6 +53,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
|||||||
const isMac = getIsMac();
|
const isMac = getIsMac();
|
||||||
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);
|
const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap"].includes(note.type);
|
||||||
const isSearchOrBook = ["search", "book"].includes(note.type);
|
const isSearchOrBook = ["search", "book"].includes(note.type);
|
||||||
|
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -59,8 +61,14 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
|||||||
className="note-actions"
|
className="note-actions"
|
||||||
hideToggleArrow
|
hideToggleArrow
|
||||||
noSelectButtonStyle
|
noSelectButtonStyle
|
||||||
iconAction
|
iconAction>
|
||||||
>
|
|
||||||
|
{isReadOnly && <>
|
||||||
|
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
||||||
|
command={() => enableEditing()} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
</>}
|
||||||
|
|
||||||
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
|
{canBeConvertedToAttachment && <ConvertToAttachment note={note} /> }
|
||||||
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
{note.type === "render" && <CommandItem command="renderActiveNote" icon="bx bx-extension" text={t("note_actions.re_render_note")} />}
|
||||||
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
<CommandItem command="findInText" icon="bx bx-search" disabled={!isSearchable} text={t("note_actions.search_in_note")} />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"
|
|||||||
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
import { useNoteContext, useNoteProperty, useStaticTooltipWithKeyboardShortcut, useTriliumEvents } from "../react/hooks";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
|
|
||||||
import { numberObjectsInPlace } from "../../services/utils";
|
import { Indexed, numberObjectsInPlace } from "../../services/utils";
|
||||||
import { EventNames } from "../../components/app_context";
|
import { EventNames } from "../../components/app_context";
|
||||||
import NoteActions from "./NoteActions";
|
import NoteActions from "./NoteActions";
|
||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
@@ -11,30 +11,47 @@ import { TabConfiguration, TitleContext } from "./ribbon-interface";
|
|||||||
|
|
||||||
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>(RIBBON_TAB_DEFINITIONS);
|
||||||
|
|
||||||
|
interface ComputedTab extends Indexed<TabConfiguration> {
|
||||||
|
shouldShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Ribbon() {
|
export default function Ribbon() {
|
||||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
|
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext();
|
||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
const titleContext: TitleContext = { note };
|
|
||||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
|
||||||
const computedTabs = useMemo(
|
const [ computedTabs, setComputedTabs ] = useState<ComputedTab[]>();
|
||||||
() => TAB_CONFIGURATION.map(tab => {
|
const titleContext: TitleContext = useMemo(() => ({
|
||||||
const shouldShow = typeof tab.show === "boolean" ? tab.show : tab.show?.(titleContext);
|
note,
|
||||||
return {
|
noteContext
|
||||||
|
}), [ note, noteContext ]);
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
const computedTabs: ComputedTab[] = [];
|
||||||
|
for (const tab of TAB_CONFIGURATION) {
|
||||||
|
const shouldShow = await shouldShowTab(tab.show, titleContext);
|
||||||
|
computedTabs.push({
|
||||||
...tab,
|
...tab,
|
||||||
shouldShow
|
shouldShow: !!shouldShow
|
||||||
}
|
});
|
||||||
}),
|
}
|
||||||
[ titleContext, note, noteType ]);
|
setComputedTabs(computedTabs);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [ note, noteType, isReadOnlyTemporarilyDisabled ]);
|
||||||
|
|
||||||
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
// Automatically activate the first ribbon tab that needs to be activated whenever a note changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!computedTabs) return;
|
||||||
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
|
const tabToActivate = computedTabs.find(tab => tab.shouldShow && (typeof tab.activate === "boolean" ? tab.activate : tab.activate?.(titleContext)));
|
||||||
setActiveTabIndex(tabToActivate?.index);
|
setActiveTabIndex(tabToActivate?.index);
|
||||||
}, [ note?.noteId ]);
|
}, [ computedTabs, note?.noteId ]);
|
||||||
|
|
||||||
// Register keyboard shortcuts.
|
// Register keyboard shortcuts.
|
||||||
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
|
const eventsToListenTo = useMemo(() => TAB_CONFIGURATION.filter(config => config.toggleCommand).map(config => config.toggleCommand) as EventNames[], []);
|
||||||
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
|
useTriliumEvents(eventsToListenTo, useCallback((e, toggleCommand) => {
|
||||||
|
if (!computedTabs) return;
|
||||||
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
|
const correspondingTab = computedTabs.find(tab => tab.toggleCommand === toggleCommand);
|
||||||
if (correspondingTab) {
|
if (correspondingTab) {
|
||||||
if (activeTabIndex !== correspondingTab.index) {
|
if (activeTabIndex !== correspondingTab.index) {
|
||||||
@@ -51,7 +68,7 @@ export default function Ribbon() {
|
|||||||
<>
|
<>
|
||||||
<div className="ribbon-top-row">
|
<div className="ribbon-top-row">
|
||||||
<div className="ribbon-tab-container">
|
<div className="ribbon-tab-container">
|
||||||
{computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
|
{computedTabs && computedTabs.map(({ title, icon, index, toggleCommand, shouldShow }) => (
|
||||||
shouldShow && <RibbonTab
|
shouldShow && <RibbonTab
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={typeof title === "string" ? title : title(titleContext)}
|
title={typeof title === "string" ? title : title(titleContext)}
|
||||||
@@ -74,7 +91,7 @@ export default function Ribbon() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ribbon-body-container">
|
<div className="ribbon-body-container">
|
||||||
{computedTabs.map(tab => {
|
{computedTabs && computedTabs.map(tab => {
|
||||||
const isActive = tab.index === activeTabIndex;
|
const isActive = tab.index === activeTabIndex;
|
||||||
if (!isActive && !tab.stayInDom) {
|
if (!isActive && !tab.stayInDom) {
|
||||||
return;
|
return;
|
||||||
@@ -129,3 +146,9 @@ function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: stri
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function shouldShowTab(showConfig: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined), context: TitleContext) {
|
||||||
|
if (showConfig === null || showConfig === undefined) return true;
|
||||||
|
if (typeof showConfig === "boolean") return showConfig;
|
||||||
|
if ("then" in showConfig) return await showConfig(context);
|
||||||
|
return showConfig(context);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
|
|||||||
{
|
{
|
||||||
title: t("classic_editor_toolbar.title"),
|
title: t("classic_editor_toolbar.title"),
|
||||||
icon: "bx bx-text",
|
icon: "bx bx-text",
|
||||||
show: ({ note }) => note?.type === "text" && options.get("textNoteEditorType") === "ckeditor-classic",
|
show: async ({ note, noteContext }) => note?.type === "text"
|
||||||
|
&& options.get("textNoteEditorType") === "ckeditor-classic"
|
||||||
|
&& !(await noteContext?.isReadOnly()),
|
||||||
toggleCommand: "toggleRibbonTabClassicEditor",
|
toggleCommand: "toggleRibbonTabClassicEditor",
|
||||||
content: FormattingToolbar,
|
content: FormattingToolbar,
|
||||||
activate: true,
|
activate: true,
|
||||||
|
|||||||
@@ -238,11 +238,6 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Focus on show.
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => editorRef.current?.focus(), 0);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Interaction with CKEditor.
|
// Interaction with CKEditor.
|
||||||
useLegacyImperativeHandlers(useMemo(() => ({
|
useLegacyImperativeHandlers(useMemo(() => ({
|
||||||
loadReferenceLinkTitle: async ($el: JQuery<HTMLElement>, href: string) => {
|
loadReferenceLinkTitle: async ($el: JQuery<HTMLElement>, href: string) => {
|
||||||
@@ -363,6 +358,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
}}
|
}}
|
||||||
onKeyDown={() => attributeDetailWidget.hide()}
|
onKeyDown={() => attributeDetailWidget.hide()}
|
||||||
onBlur={() => save()}
|
onBlur={() => save()}
|
||||||
|
onInitialized={() => editorRef.current?.focus()}
|
||||||
disableNewlines disableSpellcheck
|
disableNewlines disableSpellcheck
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { useNoteContext } from "../../react/hooks";
|
import { useNoteContext } from "../../react/hooks";
|
||||||
import { TabContext, TitleContext } from "../ribbon-interface";
|
import { TabContext } from "../ribbon-interface";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { RIBBON_TAB_DEFINITIONS } from "../RibbonDefinition";
|
import { RIBBON_TAB_DEFINITIONS } from "../RibbonDefinition";
|
||||||
|
import { shouldShowTab } from "../Ribbon";
|
||||||
|
|
||||||
interface StandaloneRibbonAdapterProps {
|
interface StandaloneRibbonAdapterProps {
|
||||||
component: (props: TabContext) => ComponentChildren;
|
component: (props: TabContext) => ComponentChildren;
|
||||||
@@ -16,10 +17,11 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
|
|||||||
const Component = component;
|
const Component = component;
|
||||||
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
|
const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId } = useNoteContext();
|
||||||
const definition = useMemo(() => RIBBON_TAB_DEFINITIONS.find(def => def.content === component), [ component ]);
|
const definition = useMemo(() => RIBBON_TAB_DEFINITIONS.find(def => def.content === component), [ component ]);
|
||||||
const [ shown, setShown ] = useState(unwrapShown(definition?.show, { note }));
|
const [ shown, setShown ] = useState<boolean | null | undefined>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShown(unwrapShown(definition?.show, { note }));
|
if (!definition) return;
|
||||||
|
shouldShowTab(definition.show, { note, noteContext }).then(setShown);
|
||||||
}, [ note ]);
|
}, [ note ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,9 +37,3 @@ export default function StandaloneRibbonAdapter({ component }: StandaloneRibbonA
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function unwrapShown(value: boolean | ((context: TitleContext) => boolean | null | undefined) | undefined, context: TitleContext) {
|
|
||||||
if (!value) return true;
|
|
||||||
if (typeof value === "boolean") return value;
|
|
||||||
return !!value(context);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,13 +16,14 @@ export interface TabContext {
|
|||||||
|
|
||||||
export interface TitleContext {
|
export interface TitleContext {
|
||||||
note: FNote | null | undefined;
|
note: FNote | null | undefined;
|
||||||
|
noteContext: NoteContext | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TabConfiguration {
|
export interface TabConfiguration {
|
||||||
title: string | ((context: TitleContext) => string);
|
title: string | ((context: TitleContext) => string);
|
||||||
icon: string;
|
icon: string;
|
||||||
content: (context: TabContext) => VNode | false;
|
content: (context: TabContext) => VNode | false;
|
||||||
show: boolean | ((context: TitleContext) => boolean | null | undefined);
|
show: boolean | ((context: TitleContext) => Promise<boolean | null | undefined> | boolean | null | undefined);
|
||||||
toggleCommand?: KeyboardActionNames;
|
toggleCommand?: KeyboardActionNames;
|
||||||
activate?: boolean | ((context: TitleContext) => boolean);
|
activate?: boolean | ((context: TitleContext) => boolean);
|
||||||
/**
|
/**
|
||||||
|
|||||||
7
apps/client/src/widgets/shared_info.css
Normal file
7
apps/client/src/widgets/shared_info.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.shared-info-widget {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shared-info-widget button {
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import "./shared_info.css";
|
||||||
import { t } from "../services/i18n";
|
import { t } from "../services/i18n";
|
||||||
import Alert from "./react/Alert";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
|
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
|
||||||
import FNote from "../entities/fnote";
|
|
||||||
import attributes from "../services/attributes";
|
import attributes from "../services/attributes";
|
||||||
import RawHtml from "./react/RawHtml";
|
import FNote from "../entities/fnote";
|
||||||
import HelpButton from "./react/HelpButton";
|
import HelpButton from "./react/HelpButton";
|
||||||
|
import InfoBar from "./react/InfoBar";
|
||||||
|
import RawHtml from "./react/RawHtml";
|
||||||
|
|
||||||
export default function SharedInfo() {
|
export default function SharedInfo() {
|
||||||
const { note } = useNoteContext();
|
const { note } = useNoteContext();
|
||||||
@@ -35,7 +36,7 @@ export default function SharedInfo() {
|
|||||||
link = `${location.protocol}//${host}${location.pathname}share/${shareId}`;
|
link = `${location.protocol}//${host}${location.pathname}share/${shareId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLink(`<a href="${link}" class="external">${link}</a>`);
|
setLink(`<a href="${link}" class="external tn-link">${link}</a>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(refresh, [ note ]);
|
useEffect(refresh, [ note ]);
|
||||||
@@ -48,20 +49,14 @@ export default function SharedInfo() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert className="shared-info-widget" type="warning" style={{
|
<InfoBar className="shared-info-widget" type="subtle" style={{display: (!link) ? "none" : undefined}}>
|
||||||
contain: "none",
|
|
||||||
margin: "10px",
|
|
||||||
padding: "10px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
display: !link ? "none" : undefined
|
|
||||||
}}>
|
|
||||||
{link && (
|
{link && (
|
||||||
<RawHtml html={syncServerHost
|
<RawHtml html={syncServerHost
|
||||||
? t("shared_info.shared_publicly", { link })
|
? t("shared_info.shared_publicly", { link })
|
||||||
: t("shared_info.shared_locally", { link })} />
|
: t("shared_info.shared_locally", { link })} />
|
||||||
)}
|
)}
|
||||||
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
<HelpButton helpPage="R9pX4DGra2Vt" style={{ width: "24px", height: "24px" }} />
|
||||||
</Alert>
|
</InfoBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import protected_session_holder from "../../services/protected_session_holder.js
|
|||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
<div class="canvas-widget note-detail-canvas note-detail-printable note-detail">
|
||||||
<style>
|
<style>
|
||||||
|
.canvas-widget {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.excalidraw .App-menu_top .buttonList {
|
.excalidraw .App-menu_top .buttonList {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,7 +284,8 @@ function SmoothScrollEnabledOption() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function MaxContentWidth() {
|
function MaxContentWidth() {
|
||||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
const [maxContentWidth, setMaxContentWidth] = useTriliumOption("maxContentWidth");
|
||||||
|
const [centerContent, setCenterContent] = useTriliumOptionBool("centerContent");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsSection title={t("max_content_width.title")}>
|
<OptionsSection title={t("max_content_width.title")}>
|
||||||
@@ -300,9 +301,9 @@ function MaxContentWidth() {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<p>
|
<FormCheckbox label={t("max_content_width.centerContent")}
|
||||||
{t("max_content_width.apply_changes_description")} <Button text={t("max_content_width.reload_button")} size="micro" onClick={reloadFrontendApp} />
|
currentValue={centerContent}
|
||||||
</p>
|
onChange={setCenterContent} />
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ function TokenList({ tokens }: { tokens: EtapiToken[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
tokens.length ? (
|
tokens.length ? (
|
||||||
<div style={{ overflow: "auto", height: "500px"}}>
|
<div style={{ overflow: "auto"}}>
|
||||||
<table className="table table-stripped">
|
<table className="table table-stripped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ function EditorFeatures() {
|
|||||||
return (
|
return (
|
||||||
<OptionsSection title={t("editorfeatures.title")}>
|
<OptionsSection title={t("editorfeatures.title")}>
|
||||||
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="emoji-completion-enabled" optionName="textNoteEmojiCompletionEnabled" label={t("editorfeatures.emoji_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
||||||
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="note-completion-enabled" optionName="textNoteCompletionEnabled" label={t("editorfeatures.note_completion_enabled")} description={t("editorfeatures.note_completion_description")} />
|
||||||
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.emoji_completion_description")} />
|
<EditorFeature name="slash-commands-enabled" optionName="textNoteSlashCommandsEnabled" label={t("editorfeatures.slash_commands_enabled")} description={t("editorfeatures.slash_commands_description")} />
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const TPL = /*html*/`
|
|||||||
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
|
body.heading-style-underline .note-detail-readonly-text h6 { border-bottom: 1px solid var(--main-border-color); }
|
||||||
|
|
||||||
.note-detail-readonly-text {
|
.note-detail-readonly-text {
|
||||||
|
|
||||||
padding-inline-start: 24px;
|
padding-inline-start: 24px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
font-family: var(--detail-font-family);
|
font-family: var(--detail-font-family);
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ const linkOverlays = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="note-detail-relation-map note-detail-printable">
|
<div class="note-detail-relation-map full-height note-detail-printable">
|
||||||
<div class="relation-map-wrapper">
|
<div class="relation-map-wrapper">
|
||||||
<div class="relation-map-container"></div>
|
<div class="relation-map-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ const config: ForgeConfig = {
|
|||||||
"--share=network",
|
"--share=network",
|
||||||
// System notifications with libnotify
|
// System notifications with libnotify
|
||||||
"--talk-name=org.freedesktop.Notifications",
|
"--talk-name=org.freedesktop.Notifications",
|
||||||
|
// System tray
|
||||||
|
"--talk-name=org.kde.StatusNotifierWatcher"
|
||||||
],
|
],
|
||||||
modules: [
|
modules: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/desktop",
|
"name": "@triliumnext/desktop",
|
||||||
"version": "0.99.3",
|
"version": "0.99.5",
|
||||||
"description": "Build your personal knowledge base with Trilium Notes",
|
"description": "Build your personal knowledge base with Trilium Notes",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"build": "tsx scripts/build.ts",
|
"build": "tsx scripts/build.ts",
|
||||||
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
|
"start-prod": "pnpm build && cross-env TRILIUM_DATA_DIR=data TRILIUM_PORT=37841 ELECTRON_IS_DEV=0 electron dist",
|
||||||
"electron-forge:make": "pnpm build && electron-forge make dist",
|
"electron-forge:make": "pnpm build && electron-forge make dist",
|
||||||
"electron-forge:make-flatpak": "pnpm build && electron-forge make dist --targets=@electron-forge/maker-flatpak",
|
"electron-forge:make-flatpak": "pnpm build && DEBUG=* electron-forge make dist --targets=@electron-forge/maker-flatpak",
|
||||||
"electron-forge:package": "pnpm build && electron-forge package dist",
|
"electron-forge:package": "pnpm build && electron-forge package dist",
|
||||||
"electron-forge:start": "pnpm build && electron-forge start dist",
|
"electron-forge:start": "pnpm build && electron-forge start dist",
|
||||||
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
|
"e2e": "pnpm build && cross-env TRILIUM_INTEGRATION_TEST=memory-no-store TRILIUM_PORT=8082 TRILIUM_DATA_DIR=data-e2e ELECTRON_IS_DEV=0 playwright test"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"@triliumnext/commons": "workspace:*",
|
"@triliumnext/commons": "workspace:*",
|
||||||
"@triliumnext/server": "workspace:*",
|
"@triliumnext/server": "workspace:*",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.5.0",
|
"electron": "38.6.0",
|
||||||
"@electron-forge/cli": "7.10.2",
|
"@electron-forge/cli": "7.10.2",
|
||||||
"@electron-forge/maker-deb": "7.10.2",
|
"@electron-forge/maker-deb": "7.10.2",
|
||||||
"@electron-forge/maker-dmg": "7.10.2",
|
"@electron-forge/maker-dmg": "7.10.2",
|
||||||
|
|||||||
@@ -39,11 +39,16 @@ async function main() {
|
|||||||
app.commandLine.appendSwitch("disable-smooth-scrolling");
|
app.commandLine.appendSwitch("disable-smooth-scrolling");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
|
||||||
// See https://github.com/electron/electron/issues/46538 for more info.
|
|
||||||
if (process.platform === "linux") {
|
if (process.platform === "linux") {
|
||||||
app.setName(PRODUCT_NAME);
|
app.setName(PRODUCT_NAME);
|
||||||
|
|
||||||
|
// Electron 36 crashes with "Using GTK 2/3 and GTK 4 in the same process is not supported" on some distributions.
|
||||||
|
// See https://github.com/electron/electron/issues/46538 for more info.
|
||||||
app.commandLine.appendSwitch("gtk-version", "3");
|
app.commandLine.appendSwitch("gtk-version", "3");
|
||||||
|
|
||||||
|
// Enable global shortcuts in Flatpak
|
||||||
|
// the app runs in a Wayland session.
|
||||||
|
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
|||||||
3
apps/edit-docs/demo/!!!meta.json
vendored
3
apps/edit-docs/demo/!!!meta.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"formatVersion": 2,
|
"formatVersion": 2,
|
||||||
"appVersion": "0.99.2",
|
"appVersion": "0.99.3",
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"isClone": false,
|
"isClone": false,
|
||||||
@@ -2700,6 +2700,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"format": "html",
|
"format": "html",
|
||||||
|
"dataFileName": "Note Types.html",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"dirFileName": "Note Types",
|
"dirFileName": "Note Types",
|
||||||
"children": [
|
"children": [
|
||||||
|
|||||||
2
apps/edit-docs/demo/navigation.html
vendored
2
apps/edit-docs/demo/navigation.html
vendored
@@ -270,7 +270,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Note Types
|
<li><a href="root/Trilium%20Demo/Note%20Types.html" target="detail">Note Types</a>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
|
<li><a href="root/Trilium%20Demo/Note%20Types/Canvas.json" target="detail">Canvas</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<h2>☑️ Tasks</h2>
|
<h2>☑️ Tasks</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
<li data-list-item-id="e4b26220d6ce48997f1116dc1d1d83dc0">[…]</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
16
apps/edit-docs/demo/root/Trilium Demo.html
vendored
@@ -14,11 +14,10 @@
|
|||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
|
<figure class="image image-style-align-right image_resized" style="width:29.84%;">
|
||||||
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg" width="150"
|
<img style="aspect-ratio:150/150;" src="Trilium Demo_icon-color.svg"
|
||||||
height="150">
|
width="150" height="150">
|
||||||
</figure>
|
</figure>
|
||||||
<p><strong>Welcome to Trilium Notes!</strong>
|
<p><strong>Welcome to Trilium Notes!</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<p>This is a "demo" document packaged with Trilium to showcase some of its
|
<p>This is a "demo" document packaged with Trilium to showcase some of its
|
||||||
features and also give you some ideas on how you might structure your notes.
|
features and also give you some ideas on how you might structure your notes.
|
||||||
@@ -26,22 +25,17 @@
|
|||||||
you wish.</p>
|
you wish.</p>
|
||||||
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
|
<p>If you need any help, visit <a href="https://triliumnotes.org">triliumnotes.org</a> or
|
||||||
our <a href="https://github.com/TriliumNext">GitHub repository</a>
|
our <a href="https://github.com/TriliumNext">GitHub repository</a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<h2>Cleanup</h2>
|
<h2>Cleanup</h2>
|
||||||
|
|
||||||
<p>Once you're finished with experimenting and want to cleanup these pages,
|
<p>Once you're finished with experimenting and want to cleanup these pages,
|
||||||
you can simply delete them all.</p>
|
you can simply delete them all.</p>
|
||||||
<h2>Formatting</h2>
|
<h2>Formatting</h2>
|
||||||
|
|
||||||
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
<p>Trilium supports classic formatting like <em>italic</em>, <strong>bold</strong>, <em><strong>bold and italic</strong></em>.
|
||||||
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
You can add links pointing to <a href="https://triliumnotes.org/">external pages</a> or
|
||||||
<a
|
<a
|
||||||
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
class="reference-link" href="Trilium%20Demo/Formatting%20examples">Formatting examples</a>.</p>
|
||||||
<h3>Lists</h3>
|
<h3>Lists</h3>
|
||||||
|
|
||||||
<p><strong>Ordered:</strong>
|
<p><strong>Ordered:</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<ol>
|
<ol>
|
||||||
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
<li data-list-item-id="e877cc655d0239b8bb0f38696ad5d8abb">First Item</li>
|
||||||
@@ -56,7 +50,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p><strong>Unordered:</strong>
|
<p><strong>Unordered:</strong>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
<li data-list-item-id="e68bf4b518a16671c314a72073c3d900a">Item</li>
|
||||||
@@ -67,7 +60,6 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h3>Block quotes</h3>
|
<h3>Block quotes</h3>
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
<p>Whereof one cannot speak, thereof one must be silent”</p>
|
||||||
<p>– Ludwig Wittgenstein</p>
|
<p>– Ludwig Wittgenstein</p>
|
||||||
@@ -75,9 +67,9 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
|
<p>See also other examples like <a href="Trilium%20Demo/Formatting%20examples/School%20schedule.html">tables</a>,
|
||||||
<a
|
<a
|
||||||
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>,
|
href="Trilium%20Demo/Formatting%20examples/Checkbox%20lists.html">checkbox lists,</a> <a href="Trilium%20Demo/Formatting%20examples/Highlighting.html">highlighting</a>, <a href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and
|
||||||
<a
|
<a
|
||||||
href="Trilium%20Demo/Formatting%20examples/Code%20blocks.html">code blocks</a>and <a href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
href="Trilium%20Demo/Formatting%20examples/Math.html">math examples</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -21,8 +21,12 @@
|
|||||||
language, should that fail it is possible to manually adjust it. The color
|
language, should that fail it is possible to manually adjust it. The color
|
||||||
scheme for the syntax highlighting is adjustable in settings. </p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
|
scheme for the syntax highlighting is adjustable in settings. </p><pre><code class="language-application-javascript-env-frontend">function helloWorld() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
alert("Hello world");
|
alert("Hello world");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}</code></pre>
|
}</code></pre>
|
||||||
<p>For larger pieces of code it is better to use a code note, which uses
|
<p>For larger pieces of code it is better to use a code note, which uses
|
||||||
a fully-fledged code editor (CodeMirror). For an example of a code note,
|
a fully-fledged code editor (CodeMirror). For an example of a code note,
|
||||||
|
|||||||
21
apps/edit-docs/demo/root/Trilium Demo/Note Types.html
vendored
Normal file
21
apps/edit-docs/demo/root/Trilium Demo/Note Types.html
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="../../style.css">
|
||||||
|
<base target="_parent">
|
||||||
|
<title data-trilium-title>Note Types</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<h1 data-trilium-h1>Note Types</h1>
|
||||||
|
|
||||||
|
<div class="ck-content">
|
||||||
|
<p>T</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -13,9 +13,8 @@
|
|||||||
<h1 data-trilium-h1>Task manager</h1>
|
<h1 data-trilium-h1>Task manager</h1>
|
||||||
|
|
||||||
<div class="ck-content">
|
<div class="ck-content">
|
||||||
<p>This is a simple TODO/Task manager. You can see some description and explanation
|
<p>This is a simple TODO/Task manager. See the <a href="https://docs.triliumnotes.org/user-guide/advanced-usage/advanced-showcases/task-manager">Trilium documentation</a> for
|
||||||
here: <a href="https://github.com/zadam/trilium/wiki/Task-manager">https://github.com/zadam/trilium/wiki/Task-manager</a>
|
information on how it works.</p>
|
||||||
</p>
|
|
||||||
<p>Please note that this is meant as scripting example only and feature/bug
|
<p>Please note that this is meant as scripting example only and feature/bug
|
||||||
support is very limited.</p>
|
support is very limited.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,18 +16,32 @@
|
|||||||
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
|
<p>Documentation: <a href="http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html">http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_09_02.html</a>
|
||||||
</p><pre><code class="language-text-x-sh">#!/bin/bash
|
</p><pre><code class="language-text-x-sh">#!/bin/bash
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# This script opens 4 terminal windows.
|
# This script opens 4 terminal windows.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
i="0"
|
i="0"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
while [ $i -lt 4 ]
|
while [ $i -lt 4 ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
do
|
do
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
xterm &
|
xterm &
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
i=$[$i+1]
|
i=$[$i+1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
done</code></pre>
|
done</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
"@triliumnext/desktop": "workspace:*",
|
"@triliumnext/desktop": "workspace:*",
|
||||||
"@types/fs-extra": "11.0.4",
|
"@types/fs-extra": "11.0.4",
|
||||||
"copy-webpack-plugin": "13.0.1",
|
"copy-webpack-plugin": "13.0.1",
|
||||||
"electron": "38.5.0",
|
"electron": "38.6.0",
|
||||||
"fs-extra": "11.3.2"
|
"fs-extra": "11.3.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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",
|
"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",
|
||||||
"edit-demo": "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-demo.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
502
apps/server-e2e/src/exact_search.spec.ts
Normal file
@@ -0,0 +1,502 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import App from "./support/app";
|
||||||
|
|
||||||
|
const BASE_URL = "http://127.0.0.1:8082";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E tests for exact search functionality using the leading "=" operator.
|
||||||
|
*
|
||||||
|
* These tests validate the GitHub issue:
|
||||||
|
* - Searching for "pagio" returns many false positives (e.g., "page", "pages")
|
||||||
|
* - Searching for "=pagio" should return ONLY exact matches for "pagio"
|
||||||
|
*/
|
||||||
|
|
||||||
|
test.describe("Exact Search with Leading = Operator", () => {
|
||||||
|
let csrfToken: string;
|
||||||
|
let createdNoteIds: string[] = [];
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, context }) => {
|
||||||
|
const app = new App(page, context);
|
||||||
|
await app.goto();
|
||||||
|
|
||||||
|
// Get CSRF token
|
||||||
|
csrfToken = await page.evaluate(() => {
|
||||||
|
return (window as any).glob.csrfToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(csrfToken).toBeTruthy();
|
||||||
|
|
||||||
|
// Create test notes with specific content patterns
|
||||||
|
// Note 1: Contains exactly "pagio" in title
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pagio",
|
||||||
|
content: "This note contains the word pagio in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 2: Contains "page" (not exact match)
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with page",
|
||||||
|
content: "This note contains the word page in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 3: Contains "pages" (plural, not exact match)
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Test Note with pages",
|
||||||
|
content: "This note contains the word pages in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 4: Contains "homepage" (contains "page", not exact match)
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Homepage Note",
|
||||||
|
content: "This note is about homepage content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 5: Another note with exact "pagio" in content
|
||||||
|
const note5 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Another pagio Note",
|
||||||
|
content: "This is another note with pagio content for testing exact matches.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note5.ok()).toBeTruthy();
|
||||||
|
const note5Data = await note5.json();
|
||||||
|
createdNoteIds.push(note5Data.note.noteId);
|
||||||
|
|
||||||
|
// Note 6: Contains "pagio" in title only
|
||||||
|
const note6 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "pagio",
|
||||||
|
content: "This note has pagio as the title.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note6.ok()).toBeTruthy();
|
||||||
|
const note6Data = await note6.json();
|
||||||
|
createdNoteIds.push(note6Data.note.noteId);
|
||||||
|
|
||||||
|
// Wait a bit for indexing
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(async ({ page }) => {
|
||||||
|
// Clean up created notes
|
||||||
|
for (const noteId of createdNoteIds) {
|
||||||
|
try {
|
||||||
|
const taskId = `cleanup-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
await page.request.delete(`${BASE_URL}/api/notes/${noteId}?taskId=${taskId}&last=true`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to delete note ${noteId}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createdNoteIds = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search without = operator returns all partial matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return multiple notes including "page", "pages", "homepage"
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
result.noteTitle.includes("page") ||
|
||||||
|
result.noteTitle.includes("pagio") ||
|
||||||
|
result.noteTitle.includes("Homepage")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should find at least "page", "pages", "homepage", and "pagio" notes
|
||||||
|
expect(testResults.length).toBeGreaterThanOrEqual(4);
|
||||||
|
|
||||||
|
console.log("Quick search 'pag' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Quick search with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /quick-search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return only notes with exact "pagio" match
|
||||||
|
expect(data.searchResultNoteIds).toBeDefined();
|
||||||
|
expect(data.searchResults).toBeDefined();
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testResults = data.searchResults.filter((result: any) =>
|
||||||
|
createdNoteIds.includes(result.notePath.split("/").pop() || "")
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Quick search '=pagio' found:", testResults.length, "matching notes");
|
||||||
|
console.log("Note titles:", testResults.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find exactly 3 notes: "Test Note with pagio", "Another pagio Note", "pagio"
|
||||||
|
expect(testResults.length).toBe(3);
|
||||||
|
|
||||||
|
// Verify that none of the results contain "page" or "pages" (only "pagio")
|
||||||
|
for (const result of testResults) {
|
||||||
|
const title = result.noteTitle.toLowerCase();
|
||||||
|
const hasPageNotPagio = (title.includes("page") && !title.includes("pagio"));
|
||||||
|
expect(hasPageNotPagio).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API without = operator returns partial matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint without the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/pag`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search 'pag' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find at least 4 notes
|
||||||
|
expect(testNoteIds.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Full search API with = operator returns only exact matches", async ({ page }) => {
|
||||||
|
// Test the /search endpoint WITH the = operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/search/=pagio`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Should return an array of note IDs
|
||||||
|
expect(Array.isArray(data)).toBe(true);
|
||||||
|
|
||||||
|
// Filter to only our test notes
|
||||||
|
const testNoteIds = data.filter((id: string) => createdNoteIds.includes(id));
|
||||||
|
|
||||||
|
console.log("Full search '=pagio' found:", testNoteIds.length, "matching notes from our test set");
|
||||||
|
|
||||||
|
// Should find exactly 3 notes with exact "pagio" match
|
||||||
|
expect(testNoteIds.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search operator works with content search", async ({ page }) => {
|
||||||
|
// Create a note with "test" in title but different content
|
||||||
|
const noteWithTest = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Content",
|
||||||
|
content: "This note contains the exact word test in content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTest.ok()).toBeTruthy();
|
||||||
|
const noteWithTestData = await noteWithTest.json();
|
||||||
|
const testNoteId = noteWithTestData.note.noteId;
|
||||||
|
createdNoteIds.push(testNoteId);
|
||||||
|
|
||||||
|
// Create a note with "testing" (not exact match)
|
||||||
|
const noteWithTesting = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing More",
|
||||||
|
content: "This note has testing in the content.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteWithTesting.ok()).toBeTruthy();
|
||||||
|
const noteWithTestingData = await noteWithTesting.json();
|
||||||
|
createdNoteIds.push(noteWithTestingData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=test`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === testNoteId || noteId === noteWithTestingData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact search '=test' found our test notes:", ourTestNotes.length);
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find the note with exact "test" match, but not "testing"
|
||||||
|
// Note: This test may fail if the implementation doesn't properly handle exact matching in content
|
||||||
|
expect(ourTestNotes.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact search is case-insensitive", async ({ page }) => {
|
||||||
|
// Create notes with different case variations
|
||||||
|
const noteUpper = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "EXACT MATCH",
|
||||||
|
content: "This note has EXACT in uppercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteUpper.ok()).toBeTruthy();
|
||||||
|
const noteUpperData = await noteUpper.json();
|
||||||
|
createdNoteIds.push(noteUpperData.note.noteId);
|
||||||
|
|
||||||
|
const noteLower = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact match",
|
||||||
|
content: "This note has exact in lowercase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteLower.ok()).toBeTruthy();
|
||||||
|
const noteLowerData = await noteLower.json();
|
||||||
|
createdNoteIds.push(noteLowerData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search with exact operator in lowercase
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=exact`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteUpperData.note.noteId || noteId === noteLowerData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Case-insensitive exact search found:", ourTestNotes.length, "notes");
|
||||||
|
|
||||||
|
// Should find both uppercase and lowercase versions
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching with multi-word searches", async ({ page }) => {
|
||||||
|
// Create notes with various phrase patterns
|
||||||
|
const note1 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase",
|
||||||
|
content: "This note contains the exact phrase.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note1.ok()).toBeTruthy();
|
||||||
|
const note1Data = await note1.json();
|
||||||
|
createdNoteIds.push(note1Data.note.noteId);
|
||||||
|
|
||||||
|
const note2 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "exact phrase match",
|
||||||
|
content: "This note has exact phrase followed by more words.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note2.ok()).toBeTruthy();
|
||||||
|
const note2Data = await note2.json();
|
||||||
|
createdNoteIds.push(note2Data.note.noteId);
|
||||||
|
|
||||||
|
const note3 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "phrase exact",
|
||||||
|
content: "This note has the words in reverse order.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note3.ok()).toBeTruthy();
|
||||||
|
const note3Data = await note3.json();
|
||||||
|
createdNoteIds.push(note3Data.note.noteId);
|
||||||
|
|
||||||
|
const note4 = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "this exact and that phrase",
|
||||||
|
content: "Words are separated but both present.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(note4.ok()).toBeTruthy();
|
||||||
|
const note4Data = await note4.json();
|
||||||
|
createdNoteIds.push(note4Data.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "exact phrase"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='exact phrase'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return [note1Data.note.noteId, note2Data.note.noteId, note3Data.note.noteId, note4Data.note.noteId].includes(noteId || "");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"exact phrase\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only notes 1 and 2 (consecutive "exact phrase")
|
||||||
|
// Should NOT find note 3 (reversed order) or note 4 (words separated)
|
||||||
|
expect(ourTestNotes.length).toBe(2);
|
||||||
|
|
||||||
|
const foundTitles = ourTestNotes.map((r: any) => r.noteTitle);
|
||||||
|
expect(foundTitles).toContain("exact phrase");
|
||||||
|
expect(foundTitles).toContain("exact phrase match");
|
||||||
|
expect(foundTitles).not.toContain("phrase exact");
|
||||||
|
expect(foundTitles).not.toContain("this exact and that phrase");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Exact phrase matching respects word order", async ({ page }) => {
|
||||||
|
// Create notes to test word order sensitivity
|
||||||
|
const noteForward = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Testing Order",
|
||||||
|
content: "This is a test sentence for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteForward.ok()).toBeTruthy();
|
||||||
|
const noteForwardData = await noteForward.json();
|
||||||
|
createdNoteIds.push(noteForwardData.note.noteId);
|
||||||
|
|
||||||
|
const noteReverse = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Order Testing",
|
||||||
|
content: "A sentence test is this for verification.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteReverse.ok()).toBeTruthy();
|
||||||
|
const noteReverseData = await noteReverse.json();
|
||||||
|
createdNoteIds.push(noteReverseData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for exact phrase "test sentence"
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/='test sentence'`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === noteForwardData.note.noteId || noteId === noteReverseData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Exact phrase search '=\"test sentence\"' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the forward order note
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Testing Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Multi-word exact search without quotes", async ({ page }) => {
|
||||||
|
// Test that multi-word search with = but without quotes also does exact phrase matching
|
||||||
|
const notePhrase = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Quick Test Note",
|
||||||
|
content: "A simple note for multi word testing.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(notePhrase.ok()).toBeTruthy();
|
||||||
|
const notePhraseData = await notePhrase.json();
|
||||||
|
createdNoteIds.push(notePhraseData.note.noteId);
|
||||||
|
|
||||||
|
const noteScattered = await page.request.post(`${BASE_URL}/api/notes/root/children?target=into&targetBranchId=`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken },
|
||||||
|
data: {
|
||||||
|
title: "Word Multi Testing",
|
||||||
|
content: "Words are multi scattered in this testing example.",
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(noteScattered.ok()).toBeTruthy();
|
||||||
|
const noteScatteredData = await noteScattered.json();
|
||||||
|
createdNoteIds.push(noteScatteredData.note.noteId);
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Search for "=multi word" without quotes (parser tokenizes as two words)
|
||||||
|
const response = await page.request.get(`${BASE_URL}/api/quick-search/=multi word`, {
|
||||||
|
headers: { "x-csrf-token": csrfToken }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const ourTestNotes = data.searchResults.filter((result: any) => {
|
||||||
|
const noteId = result.notePath.split("/").pop();
|
||||||
|
return noteId === notePhraseData.note.noteId || noteId === noteScatteredData.note.noteId;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Multi-word exact search '=multi word' found:", ourTestNotes.length, "notes");
|
||||||
|
console.log("Note titles:", ourTestNotes.map((r: any) => r.noteTitle));
|
||||||
|
|
||||||
|
// Should find only the note with consecutive "multi word" phrase
|
||||||
|
expect(ourTestNotes.length).toBe(1);
|
||||||
|
expect(ourTestNotes[0].noteTitle).toBe("Quick Test Note");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@triliumnext/server",
|
"name": "@triliumnext/server",
|
||||||
"version": "0.99.3",
|
"version": "0.99.5",
|
||||||
"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.",
|
"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,
|
"private": true,
|
||||||
"main": "./src/main.ts",
|
"main": "./src/main.ts",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"@types/xml2js": "0.4.14",
|
"@types/xml2js": "0.4.14",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"axios": "1.13.1",
|
"axios": "1.13.2",
|
||||||
"bindings": "1.5.0",
|
"bindings": "1.5.0",
|
||||||
"bootstrap": "5.3.8",
|
"bootstrap": "5.3.8",
|
||||||
"chardet": "2.1.1",
|
"chardet": "2.1.1",
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"debounce": "3.0.0",
|
"debounce": "3.0.0",
|
||||||
"debug": "4.4.3",
|
"debug": "4.4.3",
|
||||||
"ejs": "3.1.10",
|
"ejs": "3.1.10",
|
||||||
"electron": "38.5.0",
|
"electron": "38.6.0",
|
||||||
"electron-debug": "4.1.0",
|
"electron-debug": "4.1.0",
|
||||||
"electron-window-state": "5.0.3",
|
"electron-window-state": "5.0.3",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
"html2plaintext": "2.1.4",
|
"html2plaintext": "2.1.4",
|
||||||
"http-proxy-agent": "7.0.2",
|
"http-proxy-agent": "7.0.2",
|
||||||
"https-proxy-agent": "7.0.6",
|
"https-proxy-agent": "7.0.6",
|
||||||
"i18next": "25.6.0",
|
"i18next": "25.6.1",
|
||||||
"i18next-fs-backend": "2.6.0",
|
"i18next-fs-backend": "2.6.0",
|
||||||
"image-type": "6.0.0",
|
"image-type": "6.0.0",
|
||||||
"ini": "6.0.0",
|
"ini": "6.0.0",
|
||||||
@@ -105,17 +105,17 @@
|
|||||||
"is-svg": "6.1.0",
|
"is-svg": "6.1.0",
|
||||||
"jimp": "1.6.0",
|
"jimp": "1.6.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"marked": "16.4.1",
|
"marked": "16.4.2",
|
||||||
"mime-types": "3.0.1",
|
"mime-types": "3.0.1",
|
||||||
"multer": "2.0.2",
|
"multer": "2.0.2",
|
||||||
"normalize-strings": "1.1.1",
|
"normalize-strings": "1.1.1",
|
||||||
"ollama": "0.6.2",
|
"ollama": "0.6.2",
|
||||||
"openai": "6.7.0",
|
"openai": "6.8.1",
|
||||||
"rand-token": "1.0.1",
|
"rand-token": "1.0.1",
|
||||||
"safe-compare": "1.1.4",
|
"safe-compare": "1.1.4",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"sanitize-html": "2.17.0",
|
"sanitize-html": "2.17.0",
|
||||||
"sax": "1.4.1",
|
"sax": "1.4.3",
|
||||||
"serve-favicon": "2.5.1",
|
"serve-favicon": "2.5.1",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"strip-bom": "5.0.0",
|
"strip-bom": "5.0.0",
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
"tmp": "0.2.5",
|
"tmp": "0.2.5",
|
||||||
"turndown": "7.2.2",
|
"turndown": "7.2.2",
|
||||||
"unescape": "1.0.1",
|
"unescape": "1.0.1",
|
||||||
"vite": "7.1.12",
|
"vite": "7.2.2",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
"xml2js": "0.6.2",
|
"xml2js": "0.6.2",
|
||||||
"yauzl": "3.2.0"
|
"yauzl": "3.2.0"
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ fi
|
|||||||
# Debug output
|
# Debug output
|
||||||
echo "Selected Arch: $ARCH"
|
echo "Selected Arch: $ARCH"
|
||||||
|
|
||||||
# Set Node.js version and architecture-specific filename
|
|
||||||
NODE_VERSION=22.16.0
|
|
||||||
|
|
||||||
script_dir=$(realpath $(dirname $0))
|
script_dir=$(realpath $(dirname $0))
|
||||||
|
|
||||||
|
# Set Node.js version and architecture-specific filename
|
||||||
|
NODE_VERSION=$(cat "../../.nvmrc")
|
||||||
BUILD_DIR="$script_dir/../dist"
|
BUILD_DIR="$script_dir/../dist"
|
||||||
DIST_DIR="$script_dir/../out"
|
DIST_DIR="$script_dir/../out"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
2
apps/server/src/assets/doc_notes/en/User Guide/!!!meta.json
generated
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user