diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 25cd18724..3a241d807 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,9 +13,9 @@ name: "CodeQL Advanced" on: push: - branches: [ "develop" ] + branches: [ "main" ] pull_request: - branches: [ "develop" ] + branches: [ "main" ] schedule: - cron: '20 7 * * 0' diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index be719993f..e9d3964c6 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,9 +1,9 @@ name: Dev on: push: - branches: [ develop ] + branches: [ main ] pull_request: - branches: [ develop ] + branches: [ main ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,8 +12,8 @@ concurrency: env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository_owner }}/notes - TEST_TAG: ${{ github.repository_owner }}/notes:test + IMAGE_NAME: ${{ github.repository}} + TEST_TAG: ${{ github.repository}}:test permissions: pull-requests: write # for PR comments @@ -39,7 +39,7 @@ jobs: - uses: nrwl/nx-set-shas@v4 - name: Check affected - run: pnpm nx affected --verbose -t typecheck build rebuild-deps + run: pnpm nx affected --verbose -t typecheck build rebuild-deps test-build test_dev: name: Test development @@ -77,6 +77,7 @@ jobs: - name: Trigger client build run: pnpm nx run client:build - name: Send client bundle stats to RelativeCI + if: false uses: relative-ci/agent-action@v3 with: webpackStatsFile: ./apps/client/dist/webpack-stats.json diff --git a/.github/workflows/main-docker.yml b/.github/workflows/main-docker.yml index e59ddc318..40c5149c7 100644 --- a/.github/workflows/main-docker.yml +++ b/.github/workflows/main-docker.yml @@ -1,7 +1,7 @@ on: push: branches: - - "develop" + - "main" - "feature/update**" - "feature/server_esm**" paths-ignore: @@ -14,8 +14,8 @@ on: env: GHCR_REGISTRY: ghcr.io DOCKERHUB_REGISTRY: docker.io - IMAGE_NAME: ${{ github.repository_owner }}/notes - TEST_TAG: ${{ github.repository_owner }}/notes:test + IMAGE_NAME: ${{ github.repository}} + TEST_TAG: ${{ github.repository}}:test permissions: contents: read @@ -83,6 +83,14 @@ jobs: - name: Run Playwright tests run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpm exec nx run server-e2e:e2e + + - name: Upload Playwright trace + if: failure() + uses: actions/upload-artifact@v4 + with: + name: Playwright trace (${{ matrix.dockerfile }}) + path: test-output/playwright/output + - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index bd0b4e0ec..1f370c360 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -12,7 +12,7 @@ on: paths: - .github/actions/build-electron/* - .github/workflows/nightly.yml - - forge.config.cjs + - forge.config.ts concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 8ea06d74b..3749f1efd 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -3,7 +3,7 @@ name: playwright on: push: branches: - - master + - main pull_request: permissions: @@ -40,4 +40,4 @@ jobs: # - run: npx nx-cloud record -- echo Hello World # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected # When you enable task distribution, run the e2e-ci task instead of e2e - - run: pnpm exec nx affected -t e2e + - run: pnpm exec nx affected -t e2e --exclude desktop-e2e diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9533621f0..1d8dcd453 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: shell: bash forge_platform: darwin - name: linux - image: ubuntu-latest + image: ubuntu-22.04 shell: bash forge_platform: linux - name: windows diff --git a/.gitignore b/.gitignore index 229b106e3..d7694258d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,5 @@ # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. -# Workaround for Nx bug: parent .gitignore files with '*' can cause -# `nx show projects` to return nothing by ignoring subprojects. -# See: https://github.com/nrwl/nx/issues/27368 -# Unignore everything to ensure Nx detects all projects -!* - # compiled output dist tmp @@ -52,3 +46,4 @@ upload *.tsbuildinfo /result +.svelte-kit \ No newline at end of file diff --git a/.nxignore b/.nxignore index bac1baa0e..7290b55e6 100644 --- a/.nxignore +++ b/.nxignore @@ -1,7 +1,2 @@ _regroup -_regroup_monorepo - -# Asset copying respects .gitignore / .nxignore for some reason. -# See https://github.com/nrwl/nx/issues/20309 -!dist -!node_modules \ No newline at end of file +_regroup_monorepo \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 13e5a892d..e64c42352 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -9,6 +9,8 @@ "redhat.vscode-yaml", "tobermory.es6-string-html", "vitest.explorer", - "yzhang.markdown-all-in-one" + "yzhang.markdown-all-in-one", + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss" ] } diff --git a/README.md b/README.md index 7caf50479..11391c89e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# TriliumNext Notes +# Trilium Notes ![GitHub Sponsors](https://img.shields.io/github/sponsors/eliandoran?style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/triliumnext/notes?style=flat-square) @@ -7,7 +7,7 @@ [English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md) -TriliumNext Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. +Trilium Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview: @@ -22,7 +22,7 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q * Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions) * Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts) * UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional) -* Direct [OpenID and TOTP integration](.docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md") for more secure login +* Direct [OpenID and TOTP integration](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md) for more secure login * [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server * there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting) * [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet @@ -153,7 +153,7 @@ Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide ## 👏 Shoutouts * [CKEditor 5](https://github.com/ckeditor/ckeditor5) - best WYSIWYG editor on the market, very interactive and listening team -* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. TriliumNext Notes would not be the same without it. +* [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it. * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map.html) and [link maps](https://triliumnext.github.io/Docs/Wiki/note-map.html#link-map) diff --git a/_regroup/bin/release-flatpack.sh b/_regroup/bin/release-flatpack.sh index f28ff7adb..31e42881b 100644 --- a/_regroup/bin/release-flatpack.sh +++ b/_regroup/bin/release-flatpack.sh @@ -24,7 +24,7 @@ if ! git diff-index --quiet HEAD --; then exit 1 fi -BASE_BRANCH=master +BASE_BRANCH=main if [[ "$VERSION" == *"beta"* ]]; then BASE_BRANCH=beta diff --git a/_regroup/bin/release.sh b/_regroup/bin/release.sh index db3c62e4b..fe9a65a36 100644 --- a/_regroup/bin/release.sh +++ b/_regroup/bin/release.sh @@ -47,11 +47,3 @@ echo "Tagging commit with $TAG" git tag $TAG git push origin $TAG - -echo "Updating master" - -git fetch -git checkout master -git reset --hard origin/master -git merge origin/develop -git push \ No newline at end of file diff --git a/_regroup/bin/translation.sh b/_regroup/bin/translation.sh index 4375303b9..15d211ea7 100644 --- a/_regroup/bin/translation.sh +++ b/_regroup/bin/translation.sh @@ -25,15 +25,16 @@ stats() { # Print the number of existing strings on the JSON files for each locale s=$(number_of_keys "${paths[0]}/en/server.json") c=$(number_of_keys "${paths[1]}/en/translation.json") - echo "| locale |server strings |client strings |" - echo "|--------|---------------|---------------|" - echo "| en | ${s} | ${c} |" + echo "| locale | server strings | client strings |" + echo "|--------|----------------|----------------|" + echo "| en | ${s} | ${c} |" + echo "|--------|----------------|----------------|" for locale in "${locales[@]}"; do s=$(number_of_keys "${paths[0]}/${locale}/server.json") c=$(number_of_keys "${paths[1]}/${locale}/translation.json") n1=$(((8 - ${#locale}) / 2)) n2=$((n1 == 1 ? n1 + 1 : n1)) - echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |" + echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |" done } @@ -78,7 +79,10 @@ file_path="$( cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit pwd -P )" -paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/") +paths=( + "${file_path}/../../apps/server/src/assets/translations/" + "${file_path}/../../apps/client/src/translations/" +) locales=(cn de es fr pt_br ro tw) if [ $# -eq 1 ]; then diff --git a/_regroup/eslint.config.js b/_regroup/eslint.config.js index 2f2b2c036..7c906beb2 100644 --- a/_regroup/eslint.config.js +++ b/_regroup/eslint.config.js @@ -44,7 +44,6 @@ export default tseslint.config( "dist/*", "docs/*", "demo/*", - "libraries/*", "src/public/app-dist/*", "src/public/app/doc_notes/*" ] diff --git a/_regroup/eslint.format.config.js b/_regroup/eslint.format.config.js index 23fbb6caf..9dbfd78b2 100644 --- a/_regroup/eslint.format.config.js +++ b/_regroup/eslint.format.config.js @@ -38,7 +38,6 @@ export default [ "dist/*", "docs/*", "demo/*", - "libraries/*", // TriliumNextTODO: check if we want to format packages here as well - for now skipping it "packages/*", "src/public/app-dist/*", diff --git a/_regroup/package.json b/_regroup/package.json index 57cca2b4e..abca80567 100644 --- a/_regroup/package.json +++ b/_regroup/package.json @@ -35,13 +35,13 @@ "chore:generate-openapi": "tsx bin/generate-openapi.js" }, "devDependencies": { - "@playwright/test": "1.53.0", - "@stylistic/eslint-plugin": "4.4.1", + "@playwright/test": "1.53.1", + "@stylistic/eslint-plugin": "5.0.0", "@types/express": "5.0.3", - "@types/node": "22.15.31", + "@types/node": "22.15.32", "@types/yargs": "17.0.33", - "@vitest/coverage-v8": "3.2.3", - "eslint": "9.28.0", + "@vitest/coverage-v8": "3.2.4", + "eslint": "9.29.0", "eslint-plugin-simple-import-sort": "12.1.1", "esm": "3.2.25", "jsdoc": "4.0.4", diff --git a/apps/client/.env b/apps/client/.env new file mode 100644 index 000000000..001b8becd --- /dev/null +++ b/apps/client/.env @@ -0,0 +1,4 @@ +# The development license key for premium CKEditor features. +# Note: This key must only be used for the Trilium Notes project. +# Expires on: 2025-09-13 +VITE_CKEDITOR_KEY=eyJhbGciOiJFUzI1NiJ9.eyJleHAiOjE3NTc3MjE1OTksImp0aSI6ImFiN2E0NjZmLWJlZGMtNDNiYy1iMzU4LTk0NGQ0YWJhY2I3ZiIsImRpc3RyaWJ1dGlvbkNoYW5uZWwiOlsic2giLCJkcnVwYWwiXSwid2hpdGVMYWJlbCI6dHJ1ZSwiZmVhdHVyZXMiOlsiRFJVUCIsIkNNVCIsIkRPIiwiRlAiLCJTQyIsIlRPQyIsIlRQTCIsIlBPRSIsIkNDIiwiTUYiLCJTRUUiLCJFQ0giLCJFSVMiXSwidmMiOiI1MzlkOWY5YyJ9.2rvKPql4hmukyXhEtWPZ8MLxKvzPIwzCdykO653g7IxRRZy2QJpeRszElZx9DakKYZKXekVRAwQKgHxwkgbE_w \ No newline at end of file diff --git a/apps/client/.env.production b/apps/client/.env.production new file mode 100644 index 000000000..efd1fd517 --- /dev/null +++ b/apps/client/.env.production @@ -0,0 +1 @@ +VITE_CKEDITOR_ENABLE_INSPECTOR=false diff --git a/apps/client/package.json b/apps/client/package.json index 0066f2780..77c7a559d 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,16 +1,16 @@ { "name": "@triliumnext/client", - "version": "0.94.1", + "version": "0.95.0", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", "author": { - "name": "TriliumNext Notes Team", + "name": "Trilium Notes Team", "email": "contact@eliandoran.me", "url": "https://github.com/TriliumNext/Notes" }, "dependencies": { - "@eslint/js": "9.28.0", + "@eslint/js": "9.29.0", "@excalidraw/excalidraw": "0.18.0", "@fullcalendar/core": "6.1.17", "@fullcalendar/daygrid": "6.1.17", @@ -18,7 +18,7 @@ "@fullcalendar/list": "6.1.17", "@fullcalendar/multimonth": "6.1.17", "@fullcalendar/timegrid": "6.1.17", - "@mermaid-js/layout-elk": "0.1.7", + "@mermaid-js/layout-elk": "0.1.8", "@mind-elixir/node-menu": "1.0.5", "@popperjs/core": "2.11.8", "@triliumnext/ckeditor5": "workspace:*", @@ -27,7 +27,7 @@ "@triliumnext/highlightjs": "workspace:*", "@triliumnext/share-theme": "workspace:*", "autocomplete.js": "0.38.1", - "bootstrap": "5.3.6", + "bootstrap": "5.3.7", "boxicons": "2.1.4", "dayjs": "1.11.13", "dayjs-plugin-utc": "0.1.2", @@ -47,8 +47,8 @@ "leaflet-gpx": "2.2.0", "mark.js": "8.11.1", "marked": "15.0.12", - "mermaid": "11.6.0", - "mind-elixir": "4.6.0", + "mermaid": "11.7.0", + "mind-elixir": "4.6.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", "preact": "10.26.9", @@ -66,7 +66,7 @@ "copy-webpack-plugin": "13.0.0", "happy-dom": "18.0.1", "script-loader": "0.7.2", - "vite-plugin-static-copy": "3.0.0" + "vite-plugin-static-copy": "3.0.2" }, "nx": { "name": "client", @@ -75,6 +75,9 @@ "dependsOn": [ "^build" ] + }, + "circular-deps": { + "command": "pnpx dpdm -T {projectRoot}/src/**/*.ts --tree=false --warning=false --skip-dynamic-imports=circular" } } } diff --git a/apps/client/src-example/app/app.element.css b/apps/client/src-example/app/app.element.css deleted file mode 100644 index 27d098404..000000000 --- a/apps/client/src-example/app/app.element.css +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Remove template code below - */ - html { - -webkit-text-size-adjust: 100%; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, - 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, - 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - line-height: 1.5; - tab-size: 4; - scroll-behavior: smooth; - } - body { - font-family: inherit; - line-height: inherit; - margin: 0; - } - h1, - h2, - p, - pre { - margin: 0; - } - *, - ::before, - ::after { - box-sizing: border-box; - border-width: 0; - border-style: solid; - border-color: currentColor; - } - h1, - h2 { - font-size: inherit; - font-weight: inherit; - } - a { - color: inherit; - text-decoration: inherit; - } - pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - } - svg { - display: block; - vertical-align: middle; - } - - svg { - shape-rendering: auto; - text-rendering: optimizeLegibility; - } - pre { - background-color: rgba(55, 65, 81, 1); - border-radius: 0.25rem; - color: rgba(229, 231, 235, 1); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, - 'Liberation Mono', 'Courier New', monospace; - overflow: scroll; - padding: 0.5rem 0.75rem; - } - - .shadow { - box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); - } - .rounded { - border-radius: 1.5rem; - } - - .wrapper { - width: 100%; - } - .container { - margin-left: auto; - margin-right: auto; - max-width: 768px; - padding-bottom: 3rem; - padding-left: 1rem; - padding-right: 1rem; - color: rgba(55, 65, 81, 1); - width: 100%; - } - #welcome { - margin-top: 2.5rem; - } - #welcome h1 { - font-size: 3rem; - font-weight: 500; - letter-spacing: -0.025em; - line-height: 1; - } - #welcome span { - display: block; - font-size: 1.875rem; - font-weight: 300; - line-height: 2.25rem; - margin-bottom: 0.5rem; - } - #hero { - align-items: center; - background-color: hsla(214, 62%, 21%, 1); - border: none; - box-sizing: border-box; - color: rgba(55, 65, 81, 1); - display: grid; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - #hero .text-container { - color: rgba(255, 255, 255, 1); - padding: 3rem 2rem; - } - #hero .text-container h2 { - font-size: 1.5rem; - line-height: 2rem; - position: relative; - } - #hero .text-container h2 svg { - color: hsla(162, 47%, 50%, 1); - height: 2rem; - left: -0.25rem; - position: absolute; - top: 0; - width: 2rem; - } - #hero .text-container h2 span { - margin-left: 2.5rem; - } - #hero .text-container a { - background-color: rgba(255, 255, 255, 1); - border-radius: 0.75rem; - color: rgba(55, 65, 81, 1); - display: inline-block; - margin-top: 1.5rem; - padding: 1rem 2rem; - text-decoration: inherit; - } - #hero .logo-container { - display: none; - justify-content: center; - padding-left: 2rem; - padding-right: 2rem; - } - #hero .logo-container svg { - color: rgba(255, 255, 255, 1); - width: 66.666667%; - } - - #middle-content { - align-items: flex-start; - display: grid; - gap: 4rem; - grid-template-columns: 1fr; - margin-top: 3.5rem; - } - - #learning-materials { - padding: 2.5rem 2rem; - } - #learning-materials h2 { - font-weight: 500; - font-size: 1.25rem; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .list-item-link { - align-items: center; - border-radius: 0.75rem; - display: flex; - margin-top: 1rem; - padding: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 100%; - } - .list-item-link svg:first-child { - margin-right: 1rem; - height: 1.5rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1.5rem; - } - .list-item-link > span { - flex-grow: 1; - font-weight: 400; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link > span > span { - color: rgba(107, 114, 128, 1); - display: block; - flex-grow: 1; - font-size: 0.75rem; - font-weight: 300; - line-height: 1rem; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - .list-item-link svg:last-child { - height: 1rem; - transition-property: all; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - width: 1rem; - } - .list-item-link:hover { - color: rgba(255, 255, 255, 1); - background-color: hsla(162, 47%, 50%, 1); - } - .list-item-link:hover > span { - } - .list-item-link:hover > span > span { - color: rgba(243, 244, 246, 1); - } - .list-item-link:hover svg:last-child { - transform: translateX(0.25rem); - } - - #other-links { - } - .button-pill { - padding: 1.5rem 2rem; - transition-duration: 300ms; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - align-items: center; - display: flex; - } - .button-pill svg { - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - flex-shrink: 0; - width: 3rem; - } - .button-pill > span { - letter-spacing: -0.025em; - font-weight: 400; - font-size: 1.125rem; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - .button-pill span span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - .button-pill:hover svg, - .button-pill:hover { - color: rgba(255, 255, 255, 1) !important; - } - #nx-console:hover { - background-color: rgba(0, 122, 204, 1); - } - #nx-console svg { - color: rgba(0, 122, 204, 1); - } - #nx-console-jetbrains { - margin-top: 2rem; - } - #nx-console-jetbrains:hover { - background-color: rgba(255, 49, 140, 1); - } - #nx-console-jetbrains svg { - color: rgba(255, 49, 140, 1); - } - #nx-repo:hover { - background-color: rgba(24, 23, 23, 1); - } - #nx-repo svg { - color: rgba(24, 23, 23, 1); - } - - #nx-cloud { - margin-bottom: 2rem; - margin-top: 2rem; - padding: 2.5rem 2rem; - } - #nx-cloud > div { - align-items: center; - display: flex; - } - #nx-cloud > div svg { - border-radius: 0.375rem; - flex-shrink: 0; - width: 3rem; - } - #nx-cloud > div h2 { - font-size: 1.125rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #nx-cloud > div h2 span { - display: block; - font-size: 0.875rem; - font-weight: 300; - line-height: 1.25rem; - } - #nx-cloud p { - font-size: 1rem; - line-height: 1.5rem; - margin-top: 1rem; - } - #nx-cloud pre { - margin-top: 1rem; - } - #nx-cloud a { - color: rgba(107, 114, 128, 1); - display: block; - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 1.5rem; - text-align: right; - } - #nx-cloud a:hover { - text-decoration: underline; - } - - #commands { - padding: 2.5rem 2rem; - - margin-top: 3.5rem; - } - #commands h2 { - font-size: 1.25rem; - font-weight: 400; - letter-spacing: -0.025em; - line-height: 1.75rem; - padding-left: 1rem; - padding-right: 1rem; - } - #commands p { - font-size: 1rem; - font-weight: 300; - line-height: 1.5rem; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - } - details { - align-items: center; - display: flex; - margin-top: 1rem; - padding-left: 1rem; - padding-right: 1rem; - width: 100%; - } - details pre > span { - color: rgba(181, 181, 181, 1); - } - summary { - border-radius: 0.5rem; - display: flex; - font-weight: 400; - padding: 0.5rem; - cursor: pointer; - transition-property: background-color, border-color, color, fill, stroke, - opacity, box-shadow, transform, filter, backdrop-filter, - -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; - } - summary:hover { - background-color: rgba(243, 244, 246, 1); - } - summary svg { - height: 1.5rem; - margin-right: 1rem; - width: 1.5rem; - } - - #love { - color: rgba(107, 114, 128, 1); - font-size: 0.875rem; - line-height: 1.25rem; - margin-top: 3.5rem; - opacity: 0.6; - text-align: center; - } - #love svg { - color: rgba(252, 165, 165, 1); - width: 1.25rem; - height: 1.25rem; - display: inline; - margin-top: -0.25rem; - } - - @media screen and (min-width: 768px) { - #hero { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - #hero .logo-container { - display: flex; - } - #middle-content { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - } diff --git a/apps/client/src-example/app/app.element.spec.ts b/apps/client/src-example/app/app.element.spec.ts deleted file mode 100644 index 2c12da184..000000000 --- a/apps/client/src-example/app/app.element.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { AppElement } from './app.element'; - -describe('AppElement', () => { - let app: AppElement; - - beforeEach(() => { - app = new AppElement(); - }); - - it('should create successfully', () => { - expect(app).toBeTruthy(); - }); - - it('should have a greeting', () => { - app.connectedCallback(); - - expect(app.querySelector('h1').innerHTML).toContain( - 'Welcome @triliumnext/client' - ); - }); -}); diff --git a/apps/client/src-example/app/app.element.ts b/apps/client/src-example/app/app.element.ts deleted file mode 100644 index fab80aa49..000000000 --- a/apps/client/src-example/app/app.element.ts +++ /dev/null @@ -1,409 +0,0 @@ -import './app.element.css'; - -export class AppElement extends HTMLElement { - public static observedAttributes = [ - - ]; - - connectedCallback() { - const title = '@triliumnext/client'; - this.innerHTML = ` -
-
- -
-

- Hello there, - Welcome ${title} 👋 -

-
- - -
-
-

- - - - You're up and running -

- What's next? -
-
- - - -
-
- - - - - -
-

Next steps

-

Here are some things you can do with Nx:

-
- - - - - Add UI library - -
# Generate UI lib
-nx g @nx/angular:lib ui
-
-# Add a component
-nx g @nx/angular:component ui/src/lib/button
-
-
- - - - - View interactive project graph - -
nx graph
-
-
- - - - - Run affected commands - -
# see what's been affected by changes
-nx affected:graph
-
-# run tests for current changes
-nx affected:test
-
-# run e2e tests for current changes
-nx affected:e2e
-
-
- -

- Carefully crafted with - - - -

-
-
- `; - } -} -customElements.define('triliumnext-root', AppElement); diff --git a/apps/client/src-example/assets/.gitkeep b/apps/client/src-example/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/client/src-example/favicon.ico b/apps/client/src-example/favicon.ico deleted file mode 100644 index 317ebcb23..000000000 Binary files a/apps/client/src-example/favicon.ico and /dev/null differ diff --git a/apps/client/src-example/index.html b/apps/client/src-example/index.html deleted file mode 100644 index e206d4837..000000000 --- a/apps/client/src-example/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Client - - - - - - - - - diff --git a/apps/client/src-example/main.ts b/apps/client/src-example/main.ts deleted file mode 100644 index fdb879ded..000000000 --- a/apps/client/src-example/main.ts +++ /dev/null @@ -1 +0,0 @@ -import './app/app.element'; diff --git a/apps/client/src-example/styles.css b/apps/client/src-example/styles.css deleted file mode 100644 index 90d4ee007..000000000 --- a/apps/client/src-example/styles.css +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index 3c7873e8b..443014572 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,5 +1,4 @@ import froca from "../services/froca.js"; -import bundleService from "../services/bundle.js"; import RootCommandExecutor from "./root_command_executor.js"; import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js"; import options from "../services/options.js"; @@ -281,6 +280,7 @@ export type CommandMappings = { buildIcon(name: string): NativeImage; }; refreshTouchBar: CommandData; + reloadTextEditor: CommandData; }; type EventMappings = { @@ -469,6 +469,7 @@ export class AppContext extends Component { this.tabManager.loadTabs(); + const bundleService = (await import("../services/bundle.js")).default; setTimeout(() => bundleService.executeStartupBundles(), 2000); } diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 11d32cf0d..3a8a54310 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -12,6 +12,7 @@ import type FNote from "../entities/fnote.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; +import { closeActiveDialog } from "../services/dialog.js"; export interface SetNoteOpts { triggerSwitchEvent?: unknown; @@ -83,7 +84,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); - utils.closeActiveDialog(); + closeActiveDialog(); this.notePath = resolvedNotePath; this.viewScope = opts.viewScope; diff --git a/apps/client/src/components/tab_manager.ts b/apps/client/src/components/tab_manager.ts index fa83470ce..0416071c6 100644 --- a/apps/client/src/components/tab_manager.ts +++ b/apps/client/src/components/tab_manager.ts @@ -688,7 +688,7 @@ export default class TabManager extends Component { const titleFragments = [ // it helps to navigate in history if note title is included in the title await activeNoteContext.getNavigationTitle(), - "TriliumNext Notes" + "Trilium Notes" ].filter(Boolean); document.title = titleFragments.join(" - "); diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 9e215821d..fd35c09b8 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,7 +1,6 @@ import server from "../services/server.js"; import noteAttributeCache from "../services/note_attribute_cache.js"; import ws from "../services/ws.js"; -import froca from "../services/froca.js"; import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; @@ -410,8 +409,8 @@ class FNote { const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({ notePath: path, isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId), - isArchived: path.some((noteId) => froca.notes[noteId].isArchived), - isSearch: path.some((noteId) => froca.notes[noteId].type === "search"), + isArchived: path.some((noteId) => this.froca.notes[noteId].isArchived), + isSearch: path.some((noteId) => this.froca.notes[noteId].type === "search"), isHidden: path.includes("_hidden") })); @@ -982,7 +981,7 @@ class FNote { continue; } - const parentNote = froca.notes[parentNoteId]; + const parentNote = this.froca.notes[parentNoteId]; if (!parentNote || parentNote.type === "search") { continue; diff --git a/apps/client/src/libraries/codemirror/hcl.js b/apps/client/src/libraries/codemirror/hcl.js deleted file mode 100644 index 04c0ea749..000000000 --- a/apps/client/src/libraries/codemirror/hcl.js +++ /dev/null @@ -1,204 +0,0 @@ -// Source: https://github.com/codemirror/codemirror5/pull/7080/files - -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: https://codemirror.net/5/LICENSE - -(function (mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); - else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); - else // Plain browser env - mod(CodeMirror); -})(function (CodeMirror) { - "use strict"; - - CodeMirror.defineMode("hcl", function (config) { - var indentUnit = config.indentUnit; - - var keywords = { - "resource": true, - "variable": true, - "output": true, - "module": true, - "provider": true, - "data": true, - "locals": true, - "terraform": true, - "if": true, - "else": true, - "for": true, - "foreach": true, - "in": true, - "true": true, - "false": true, - "null": true, - }; - - var atoms = { - "true": true, - "false": true, - "null": true, - }; - - var isOperatorChar = /[+\-*&^%:=<>!|\/]/; - - var curPunc; - - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"' || ch == "'" || ch == "`") { - state.tokenize = tokenString(ch); - return state.tokenize(stream, state); - } - if (/[\d\.]/.test(ch)) { - if (ch == ".") { - stream.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/); - } else { - stream.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/); - } - return "number"; - } - if (/[\[\]{}\(\),;\:\.]/.test(ch)) { - curPunc = ch; - return null; - } - if (ch == "/") { - if (stream.eat("*")) { - state.tokenize = tokenComment; - return tokenComment(stream, state); - } - if (stream.eat("/")) { - stream.skipToEnd(); - return "comment"; - } - } - if (isOperatorChar.test(ch)) { - stream.eatWhile(isOperatorChar); - return "operator"; - } - stream.eatWhile(/[\w\$_\xa1-\uffff]/); - var cur = stream.current(); - if (keywords.propertyIsEnumerable(cur)) { - return "keyword"; - } - if (atoms.propertyIsEnumerable(cur)) return "atom"; - return "variable"; - } - - function tokenString(quote) { - return function (stream, state) { - var escaped = false, - next, - end = false; - while ((next = stream.next()) != null) { - if (next == quote && !escaped) { - end = true; - break; - } - escaped = !escaped && quote != "`" && next == "\\"; - } - if (end || !(escaped || quote == "`")) - state.tokenize = tokenBase; - return "string"; - }; - } - - function tokenComment(stream, state) { - var maybeEnd = false, - ch; - while (ch = stream.next()) { - if (ch == "/" && maybeEnd) { - state.tokenize = tokenBase; - break; - } - maybeEnd = (ch == "*"); - } - return "comment"; - } - - function Context(indented, column, type, align, prev) { - this.indented = indented; - this.column = column; - this.type = type; - this.align = align; - this.prev = prev; - } - function pushContext(state, col, type) { - return state.context = new Context(state.indented, col, type, null, state.context); - } - function popContext(state) { - if (!state.context.prev) return; - var t = state.context.type; - if (t == ")" || t == "]" || t == "}") - state.indented = state.context.indented; - return state.context = state.context.prev; - } - - // Interface - - return { - startState: function (basecolumn) { - return { - tokenize: null, - context: new Context((basecolumn || 0) - indentUnit, 0, "top", false), - indented: 0, - startOfLine: true - }; - }, - - token: function (stream, state) { - var ctx = state.context; - if (stream.sol()) { - if (ctx.align == null) ctx.align = false; - state.indented = stream.indentation(); - state.startOfLine = true; - } - if (stream.eatSpace()) return null; - curPunc = null; - var style = (state.tokenize || tokenBase)(stream, state); - if (style == "comment") return style; - if (ctx.align == null) ctx.align = true; - - if (curPunc == "{") pushContext(state, stream.column(), "}"); - else if (curPunc == "[") pushContext(state, stream.column(), "]"); - else if (curPunc == "(") pushContext(state, stream.column(), ")"); - else if (curPunc == "}" && ctx.type == "}") popContext(state); - else if (curPunc == ctx.type) popContext(state); - state.startOfLine = false; - return style; - }, - - indent: function (state, textAfter) { - if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; - var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); - if (firstChar == "#" || firstChar == ";") return 0; - if (stream.sol()) { - if (ctx.type == "case" && /^(?:case|default)\b/.test(textAfter)) { - state.context.type = "}"; - return ctx.indented; - } - var closing = firstChar == ctx.type; - if (ctx.align) return ctx.column + (closing ? 0 : 1); - else return ctx.indented + (closing ? 0 : indentUnit); - } - }, - - electricChars: "{}):", - closeBrackets: "()[]{}''\"\"``", - fold: "brace", - blockCommentStart: "/*", - blockCommentEnd: "*/", - lineComment: "//" - }; - }); - - CodeMirror.defineMIME("text/x-hcl", "hcl"); - CodeMirror.modeInfo.push({ - ext: [ "hcl " ], - mime: "text/x-hcl", - mode: "hcl", - name: "Terraform (HCL)" - }); - -}); diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index a8a37f462..72519233a 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -194,14 +194,15 @@ class ContextMenu { return false; }); - if (!this.isMobile) { - $item.on("mouseup", (e) =>{ + $item.on("mouseup", (e) => { + // Prevent submenu from failing to expand on mobile + if (!this.isMobile || !("items" in item && item.items)) { e.stopPropagation(); // Hide the content menu on mouse up to prevent the mouse event from propagating to the elements below. this.hide(); return false; - }); - } + } + }); if ("enabled" in item && item.enabled !== undefined && !item.enabled) { $item.addClass("disabled"); diff --git a/apps/client/src/services/bundle.ts b/apps/client/src/services/bundle.ts index e6eea7ef1..e7b88a343 100644 --- a/apps/client/src/services/bundle.ts +++ b/apps/client/src/services/bundle.ts @@ -1,6 +1,6 @@ import ScriptContext from "./script_context.js"; import server from "./server.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; import froca from "./froca.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; @@ -37,7 +37,9 @@ async function executeBundle(bundle: Bundle, originEntity?: Entity | null, $cont } catch (e: any) { const note = await froca.getNote(bundle.noteId); - toastService.showAndLogError(`Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`); + const message = `Execution of JS note "${note?.title}" with ID ${bundle.noteId} failed with error: ${e?.message}`; + showError(message); + logError(message); } } diff --git a/apps/client/src/services/clipboard.ts b/apps/client/src/services/clipboard.ts index 952ac2e22..9ca5f9d09 100644 --- a/apps/client/src/services/clipboard.ts +++ b/apps/client/src/services/clipboard.ts @@ -4,7 +4,7 @@ import froca from "./froca.js"; import linkService from "./link.js"; import utils from "./utils.js"; import { t } from "./i18n.js"; -import toast from "./toast.js"; +import { throwError } from "./ws.js"; let clipboardBranchIds: string[] = []; let clipboardMode: string | null = null; @@ -37,7 +37,7 @@ async function pasteAfter(afterBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } @@ -69,7 +69,7 @@ async function pasteInto(parentBranchId: string) { // copy will keep clipboardBranchIds and clipboardMode, so it's possible to paste into multiple places } else { - toastService.throwError(`Unrecognized clipboard mode=${clipboardMode}`); + throwError(`Unrecognized clipboard mode=${clipboardMode}`); } } diff --git a/apps/client/src/services/dialog.ts b/apps/client/src/services/dialog.ts index e2d93250f..240172f49 100644 --- a/apps/client/src/services/dialog.ts +++ b/apps/client/src/services/dialog.ts @@ -1,6 +1,41 @@ +import { Modal } from "bootstrap"; import appContext from "../components/app_context.js"; import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions } from "../widgets/dialogs/confirm.js"; import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; +import { focusSavedElement, saveFocusedElement } from "./focus.js"; + +export async function openDialog($dialog: JQuery, closeActDialog = true) { + if (closeActDialog) { + closeActiveDialog(); + glob.activeDialog = $dialog; + } + + saveFocusedElement(); + Modal.getOrCreateInstance($dialog[0]).show(); + + $dialog.on("hidden.bs.modal", () => { + const $autocompleteEl = $(".aa-input"); + if ("autocomplete" in $autocompleteEl) { + $autocompleteEl.autocomplete("close"); + } + + if (!glob.activeDialog || glob.activeDialog === $dialog) { + focusSavedElement(); + } + }); + + const keyboardActionsService = (await import("./keyboard_actions.js")).default; + keyboardActionsService.updateDisplayedShortcuts($dialog); + + return $dialog; +} + +export function closeActiveDialog() { + if (glob.activeDialog) { + Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); + glob.activeDialog = null; + } +} async function info(message: string) { return new Promise((res) => appContext.triggerCommand("showInfoDialog", { message, callback: res })); diff --git a/apps/client/src/services/focus.ts b/apps/client/src/services/focus.ts new file mode 100644 index 000000000..066c74558 --- /dev/null +++ b/apps/client/src/services/focus.ts @@ -0,0 +1,29 @@ +let $lastFocusedElement: JQuery | null; + +// perhaps there should be saved focused element per tab? +export function saveFocusedElement() { + $lastFocusedElement = $(":focus"); +} + +export function focusSavedElement() { + if (!$lastFocusedElement) { + return; + } + + if ($lastFocusedElement.hasClass("ck")) { + // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 + // the bug manifests itself in resetting the cursor position to the first character - jumping above + + const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); + + if (editor) { + editor.editing.view.focus(); + } else { + console.log("Could not find CKEditor instance to focus last element"); + } + } else { + $lastFocusedElement.focus(); + } + + $lastFocusedElement = null; +} diff --git a/apps/client/src/services/froca.ts b/apps/client/src/services/froca.ts index 131cec06f..6bbc3a50d 100644 --- a/apps/client/src/services/froca.ts +++ b/apps/client/src/services/froca.ts @@ -245,6 +245,10 @@ class FrocaImpl implements Froca { } async getNotes(noteIds: string[] | JQuery, silentNotFoundError = false): Promise { + if (noteIds.length === 0) { + return []; + } + noteIds = Array.from(new Set(noteIds)); // make unique const missingNoteIds = noteIds.filter((noteId) => !this.notes[noteId]); diff --git a/apps/client/src/services/i18n.spec.ts b/apps/client/src/services/i18n.spec.ts index 9143097be..e64605949 100644 --- a/apps/client/src/services/i18n.spec.ts +++ b/apps/client/src/services/i18n.spec.ts @@ -1,6 +1,7 @@ import { LOCALES } from "@triliumnext/commons"; import { readFileSync } from "fs"; import { join } from "path"; +import { describe, expect, it } from "vitest"; describe("i18n", () => { it("translations are valid JSON", () => { diff --git a/apps/client/src/services/image.ts b/apps/client/src/services/image.ts index 3cf1424d5..f13a9a3c7 100644 --- a/apps/client/src/services/image.ts +++ b/apps/client/src/services/image.ts @@ -1,5 +1,5 @@ import { t } from "./i18n.js"; -import toastService from "./toast.js"; +import toastService, { showError } from "./toast.js"; function copyImageReferenceToClipboard($imageWrapper: JQuery) { try { @@ -11,7 +11,9 @@ function copyImageReferenceToClipboard($imageWrapper: JQuery) { if (success) { toastService.showMessage(t("image.copied-to-clipboard")); } else { - toastService.showAndLogError(t("image.cannot-copy")); + const message = t("image.cannot-copy"); + showError(message); + logError(message); } } finally { window.getSelection()?.removeAllRanges(); diff --git a/apps/client/src/services/note_autocomplete.ts b/apps/client/src/services/note_autocomplete.ts index 4ffff8594..d6eb4df0e 100644 --- a/apps/client/src/services/note_autocomplete.ts +++ b/apps/client/src/services/note_autocomplete.ts @@ -289,13 +289,11 @@ function initNoteAutocomplete($el: JQuery, options?: Options) { } if (suggestion.action === "create-note") { - const { success, noteType, templateNoteId } = await noteCreateService.chooseNoteType(); - + const { success, noteType, templateNoteId, notePath } = await noteCreateService.chooseNoteType(); if (!success) { return; } - - const { note } = await noteCreateService.createNote(suggestion.parentNoteId, { + const { note } = await noteCreateService.createNote( notePath || suggestion.parentNoteId, { title: suggestion.noteTitle, activate: false, type: noteType, diff --git a/apps/client/src/services/note_create.ts b/apps/client/src/services/note_create.ts index 5fa262553..6ce92bc0d 100644 --- a/apps/client/src/services/note_create.ts +++ b/apps/client/src/services/note_create.ts @@ -116,7 +116,7 @@ async function chooseNoteType() { } async function createNoteWithTypePrompt(parentNotePath: string, options: CreateNoteOpts = {}) { - const { success, noteType, templateNoteId } = await chooseNoteType(); + const { success, noteType, templateNoteId, notePath } = await chooseNoteType(); if (!success) { return; @@ -125,7 +125,7 @@ async function createNoteWithTypePrompt(parentNotePath: string, options: CreateN options.type = noteType; options.templateNoteId = templateNoteId; - return await createNote(parentNotePath, options); + return await createNote(notePath || parentNotePath, options); } /* If the first element is heading, parse it out and use it as a new heading. */ diff --git a/apps/client/src/services/note_types.ts b/apps/client/src/services/note_types.ts index 4de0bd8bf..e9905063c 100644 --- a/apps/client/src/services/note_types.ts +++ b/apps/client/src/services/note_types.ts @@ -4,6 +4,8 @@ import { t } from "./i18n.js"; import type { MenuItem } from "../menus/context_menu.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js"; +const SEPARATOR = { title: "----" }; + async function getNoteTypeItems(command?: TreeCommandNames) { const items: MenuItem[] = [ { title: t("note_types.text"), command, type: "text", uiIcon: "bx bx-note" }, @@ -18,25 +20,59 @@ async function getNoteTypeItems(command?: TreeCommandNames) { { title: t("note_types.web-view"), command, type: "webView", uiIcon: "bx bx-globe-alt" }, { title: t("note_types.mind-map"), command, type: "mindMap", uiIcon: "bx bx-sitemap" }, { title: t("note_types.geo-map"), command, type: "geoMap", uiIcon: "bx bx-map-alt" }, + ...await getBuiltInTemplates(command), + ...await getUserTemplates(command) ]; + return items; +} + +async function getUserTemplates(command?: TreeCommandNames) { const templateNoteIds = await server.get("search-templates"); const templateNotes = await froca.getNotes(templateNoteIds); - - if (templateNotes.length > 0) { - items.push({ title: "----" }); - - for (const templateNote of templateNotes) { - items.push({ - title: templateNote.title, - uiIcon: templateNote.getIcon(), - command: command, - type: templateNote.type, - templateNoteId: templateNote.noteId - }); - } + if (templateNotes.length === 0) { + return []; } + const items: MenuItem[] = [ + SEPARATOR + ]; + for (const templateNote of templateNotes) { + items.push({ + title: templateNote.title, + uiIcon: templateNote.getIcon(), + command: command, + type: templateNote.type, + templateNoteId: templateNote.noteId + }); + } + return items; +} + +async function getBuiltInTemplates(command?: TreeCommandNames) { + const templatesRoot = await froca.getNote("_templates"); + if (!templatesRoot) { + console.warn("Unable to find template root."); + return []; + } + + const childNotes = await templatesRoot.getChildNotes(); + if (childNotes.length === 0) { + return []; + } + + const items: MenuItem[] = [ + SEPARATOR + ]; + for (const templateNote of childNotes) { + items.push({ + title: templateNote.title, + uiIcon: templateNote.getIcon(), + command: command, + type: templateNote.type, + templateNoteId: templateNote.noteId + }); + } return items; } diff --git a/apps/client/src/services/script_context.ts b/apps/client/src/services/script_context.ts index 27a8a7d44..7c15db1ae 100644 --- a/apps/client/src/services/script_context.ts +++ b/apps/client/src/services/script_context.ts @@ -1,4 +1,4 @@ -import FrontendScriptApi, { type Entity } from "./frontend_script_api.js"; +import type { Entity } from "./frontend_script_api.js"; import utils from "./utils.js"; import froca from "./froca.js"; @@ -14,6 +14,8 @@ async function ScriptContext(startNoteId: string, allNoteIds: string[], originEn throw new Error(`Could not find start note ${startNoteId}.`); } + const FrontendScriptApi = (await import("./frontend_script_api.js")).default; + return { modules: modules, notes: utils.toObject(allNotes, (note) => [note.noteId, note]), diff --git a/apps/client/src/services/server.ts b/apps/client/src/services/server.ts index f577fbfb4..cb557b19b 100644 --- a/apps/client/src/services/server.ts +++ b/apps/client/src/services/server.ts @@ -276,7 +276,8 @@ async function reportError(method: string, url: string, statusCode: number, resp } else { const title = `${statusCode} ${method} ${url}`; toastService.showErrorTitleAndMessage(title, messageStr); - toastService.throwError(`${title} - ${message}`); + const { throwError } = await import("./ws.js"); + throwError(`${title} - ${message}`); } } diff --git a/apps/client/src/services/toast.ts b/apps/client/src/services/toast.ts index 18cc876a9..66b3f7f50 100644 --- a/apps/client/src/services/toast.ts +++ b/apps/client/src/services/toast.ts @@ -78,13 +78,7 @@ function showMessage(message: string, delay = 2000) { }); } -function showAndLogError(message: string, delay = 10000) { - showError(message, delay); - - ws.logError(message); -} - -function showError(message: string, delay = 10000) { +export function showError(message: string, delay = 10000) { console.log(utils.now(), "error: ", message); toast({ @@ -108,18 +102,10 @@ function showErrorTitleAndMessage(title: string, message: string, delay = 10000) }); } -function throwError(message: string) { - ws.logError(message); - - throw new Error(message); -} - export default { showMessage, showError, showErrorTitleAndMessage, - showAndLogError, - throwError, showPersistent, closePersistent }; diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 2cfa07ee8..c7d37e4d9 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -1,5 +1,4 @@ import dayjs from "dayjs"; -import { Modal } from "bootstrap"; import type { ViewScope } from "./link.js"; const SVG_MIME = "image/svg+xml"; @@ -275,71 +274,6 @@ function getMimeTypeClass(mime: string) { return `mime-${mime.toLowerCase().replace(/[\W_]+/g, "-")}`; } -function closeActiveDialog() { - if (glob.activeDialog) { - Modal.getOrCreateInstance(glob.activeDialog[0]).hide(); - glob.activeDialog = null; - } -} - -let $lastFocusedElement: JQuery | null; - -// perhaps there should be saved focused element per tab? -function saveFocusedElement() { - $lastFocusedElement = $(":focus"); -} - -function focusSavedElement() { - if (!$lastFocusedElement) { - return; - } - - if ($lastFocusedElement.hasClass("ck")) { - // must handle CKEditor separately because of this bug: https://github.com/ckeditor/ckeditor5/issues/607 - // the bug manifests itself in resetting the cursor position to the first character - jumping above - - const editor = $lastFocusedElement.closest(".ck-editor__editable").prop("ckeditorInstance"); - - if (editor) { - editor.editing.view.focus(); - } else { - console.log("Could not find CKEditor instance to focus last element"); - } - } else { - $lastFocusedElement.focus(); - } - - $lastFocusedElement = null; -} - -async function openDialog($dialog: JQuery, closeActDialog = true) { - if (closeActDialog) { - closeActiveDialog(); - glob.activeDialog = $dialog; - } - - saveFocusedElement(); - Modal.getOrCreateInstance($dialog[0]).show(); - - $dialog.on("hidden.bs.modal", () => { - const $autocompleteEl = $(".aa-input"); - if ("autocomplete" in $autocompleteEl) { - $autocompleteEl.autocomplete("close"); - } - - if (!glob.activeDialog || glob.activeDialog === $dialog) { - focusSavedElement(); - } - }); - - // TODO: Fix once keyboard_actions is ported. - // @ts-ignore - const keyboardActionsService = (await import("./keyboard_actions.js")).default; - keyboardActionsService.updateDisplayedShortcuts($dialog); - - return $dialog; -} - function isHtmlEmpty(html: string) { if (!html) { return true; @@ -825,10 +759,6 @@ export default { setCookie, getNoteTypeClass, getMimeTypeClass, - closeActiveDialog, - openDialog, - saveFocusedElement, - focusSavedElement, isHtmlEmpty, clearBrowserCache, copySelectionToClipboard, diff --git a/apps/client/src/services/ws.ts b/apps/client/src/services/ws.ts index ccfd19592..dcd63e577 100644 --- a/apps/client/src/services/ws.ts +++ b/apps/client/src/services/ws.ts @@ -17,7 +17,7 @@ let lastProcessedEntityChangeId = window.glob.maxEntityChangeIdAtLoad; let lastPingTs: number; let frontendUpdateDataQueue: EntityChange[] = []; -function logError(message: string) { +export function logError(message: string) { console.error(utils.now(), message); // needs to be separate from .trace() if (ws && ws.readyState === 1) { @@ -301,6 +301,12 @@ setTimeout(() => { setInterval(sendPing, 1000); }, 0); +export function throwError(message: string) { + logError(message); + + throw new Error(message); +} + export default { logError, subscribeToMessages, diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 2f026ab01..0bf0588b0 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -25,6 +25,7 @@ --bs-body-font-weight: var(--main-font-weight) !important; --bs-body-color: var(--main-text-color) !important; --bs-body-bg: var(--main-background-color) !important; + --ck-mention-list-max-height: 500px; } .table { @@ -391,7 +392,7 @@ body.desktop .dropdown-menu { } body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item, -body.desktop #context-menu-container .dropdown-item > span { +body #context-menu-container .dropdown-item > span { display: flex; align-items: center; } @@ -439,10 +440,11 @@ body.desktop #context-menu-container .dropdown-item > span { border-radius: 6px; overflow: hidden; margin: 4px; + font-size: var(--monospace-font-size); } -body .cm-editor { - font-size: var(--monospace-font-size); +.cm-scroller { + font-family: var(--monospace-font-family) !important; } body .cm-editor .cm-gutters { @@ -1273,6 +1275,29 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { white-space: normal !important; } +/* Slash commands */ + +.ck.ck-slash-command-button { + padding: 0.5em 1em !important; +} + +.ck.ck-slash-command-button__text-part, +.ck.ck-template-form__text-part { + margin-left: 0.5em; + line-height: 1.2em !important; +} + +.ck.ck-slash-command-button__text-part > span, +.ck.ck-template-form__text-part > span { + line-height: inherit !important; +} + +.ck.ck-slash-command-button__text-part .ck.ck-slash-command-button__description, +.ck.ck-template-form__text-part .ck-template-form__description { + display: block; + opacity: 0.8; +} + .area-expander { display: flex; flex-direction: row; diff --git a/apps/client/src/stylesheets/theme-next/dialogs.css b/apps/client/src/stylesheets/theme-next/dialogs.css index e73c555fc..45456693b 100644 --- a/apps/client/src/stylesheets/theme-next/dialogs.css +++ b/apps/client/src/stylesheets/theme-next/dialogs.css @@ -395,4 +395,20 @@ div.tn-tool-dialog { padding-right: 12px; font-weight: normal; white-space: nowrap; +} + +/* + * NOTE TYPE CHOOSER DIALOG + */ + +.note-type-chooser-dialog div.note-type-dropdown { + /* Disable the active item highlighting since there is no use for it here */ + --active-item-text-color: initial; + --active-item-background-color: initial; + + font-size: unset; +} + +.note-type-chooser-dialog div.note-type-dropdown .dropdown-item span.bx { + margin-right: .25em; } \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next/forms.css b/apps/client/src/stylesheets/theme-next/forms.css index 446c9b89c..b09d6cb65 100644 --- a/apps/client/src/stylesheets/theme-next/forms.css +++ b/apps/client/src/stylesheets/theme-next/forms.css @@ -267,7 +267,7 @@ input::selection, } .input-group button:focus-visible, -.input-group a:focus-visible { +.input-group a:focus-visible:not(.dropdown-item) { box-shadow: unset; outline: transparent; border: transparent; @@ -349,7 +349,7 @@ select:hover, select.form-select:hover, select.form-control:hover, .select-button.dropdown-toggle.btn:hover { - background: var(--input-hover-background) var(--dropdown-arrow); + background: var(--input-hover-background) var(--dropdown-arrow,); color: var(--input-hover-color); } diff --git a/apps/client/src/stylesheets/theme-next/notes/text.css b/apps/client/src/stylesheets/theme-next/notes/text.css index f7b9e3f51..404a47fec 100644 --- a/apps/client/src/stylesheets/theme-next/notes/text.css +++ b/apps/client/src/stylesheets/theme-next/notes/text.css @@ -201,6 +201,11 @@ color: var(--menu-item-icon-color); } +/* Slash commands */ +.ck.ck-slash-command-button__text-part .ck.ck-button__label { + font-weight: bold; +} + /* Separator */ :root .ck .ck-list__separator { margin: .5em 0; diff --git a/apps/client/src/stylesheets/theme-next/pages.css b/apps/client/src/stylesheets/theme-next/pages.css index 5a34b9680..1d2d0512c 100644 --- a/apps/client/src/stylesheets/theme-next/pages.css +++ b/apps/client/src/stylesheets/theme-next/pages.css @@ -142,6 +142,12 @@ div.note-detail-empty { border: unset; } +/* NOTE ATTACHMENTS */ + +.attachment-list div.links-wrapper { + font-size: unset; +} + /* * OPTIONS PAGES */ diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 5b11839e5..c48a52290 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -354,7 +354,7 @@ body.layout-horizontal > .horizontal { } .calendar-dropdown-widget .calendar-header .calendar-month-selector .select-button { - --select-arrow-svg: ""; /* Disable the dropdown arrow */ + --select-arrow-svg: initial; /* Disable the dropdown arrow */ } @media (max-width: 992px) { @@ -1145,12 +1145,18 @@ body.mobile .note-title { /* The "Change note icon" button */ -.note-icon-widget .note-icon { +:root .note-icon-widget button.note-icon, +:root .note-icon-widget button.note-icon:hover { border: none; border-radius: 8px; } -.note-icon-widget .note-icon:hover { +/* Dropdown open */ +:root .note-icon-widget button.note-icon.show { + background: var(--ck-editor-toolbar-dropdown-button-open-background); +} + +:root .note-icon-widget button.note-icon:not(:disabled):hover { background: var(--icon-button-hover-background); color: var(--icon-button-hover-color); } diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index cf852e56b..ccc04e21b 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "关于 TriliumNext Notes", + "title": "关于 Trilium Notes", "close": "关闭", "homepage": "项目主页:", "app_version": "应用版本:", diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index ea17f0575..694e7934f 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "Über TriliumNext Notes", + "title": "Über Trilium Notes", "close": "Schließen", "homepage": "Startseite:", "app_version": "App-Version:", @@ -639,7 +639,7 @@ "reload_frontend": "Frontend neu laden", "show_hidden_subtree": "Versteckten Teilbaum anzeigen", "show_help": "Hilfe anzeigen", - "about": "Über TriliumNext Notes", + "about": "Über Trilium Notes", "logout": "Abmelden", "show-cheatsheet": "Cheatsheet anzeigen", "toggle-zen-mode": "Zen Modus" diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index ab7933617..6d3ad07a2 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "About TriliumNext Notes", + "title": "About Trilium Notes", "close": "Close", "homepage": "Homepage:", "app_version": "App version:", @@ -233,6 +233,8 @@ "move_success_message": "Selected notes have been moved into " }, "note_type_chooser": { + "change_path_prompt": "Change where to create the new note:", + "search_placeholder": "search path by name (default if empty)", "modal_title": "Choose note type", "close": "Close", "modal_body": "Choose note type / template of the new note:", @@ -641,7 +643,7 @@ "reload_frontend": "Reload Frontend", "show_hidden_subtree": "Show Hidden Subtree", "show_help": "Show Help", - "about": "About TriliumNext Notes", + "about": "About Trilium Notes", "logout": "Logout", "show-cheatsheet": "Show Cheatsheet", "toggle-zen-mode": "Zen Mode" diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 64951c3a6..45d1a6115 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "Acerca de TriliumNext Notes", + "title": "Acerca de Trilium Notes", "close": "Cerrar", "homepage": "Página principal:", "app_version": "Versión de la aplicación:", @@ -16,7 +16,7 @@ "message": "Ha ocurrido un error crítico que previene que el cliente de la aplicación inicie:\n\n{{message}}\n\nMuy probablemente es causado por un script que falla de forma inesperada. Intente iniciar la aplicación en modo seguro y atienda el error." }, "widget-error": { - "title": "No se pudo inicializar un widget", + "title": "Hubo un fallo al inicializar un widget", "message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}", "message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}" }, @@ -127,6 +127,7 @@ "collapseSubTree": "colapsar subárbol", "tabShortcuts": "Atajos de pestañas", "newTabNoteLink": "CTRL+clic - (o clic central del mouse) en el enlace de la nota abre la nota en una nueva pestaña", + "newTabWithActivationNoteLink": "Ctrl+Shift+clic - (o Shift+clic de rueda de ratón) en el enlace de la nota abre y activa la nota en una nueva pestaña", "onlyInDesktop": "Solo en escritorio (compilación con Electron)", "openEmptyTab": "abrir pestaña vacía", "closeActiveTab": "cerrar pestaña activa", @@ -232,6 +233,8 @@ "move_success_message": "Las notas seleccionadas se han movido a " }, "note_type_chooser": { + "change_path_prompt": "Cambiar donde se creará la nueva nota:", + "search_placeholder": "ruta de búsqueda por nombre (por defecto si está vacío)", "modal_title": "Elija el tipo de nota", "close": "Cerrar", "modal_body": "Elija el tipo de nota/plantilla de la nueva nota:", @@ -274,9 +277,9 @@ "revision_last_edited": "Esta revisión se editó por última vez en {{date}}", "confirm_delete_all": "¿Quiere eliminar todas las revisiones de esta nota?", "no_revisions": "Aún no hay revisiones para esta nota...", - "restore_button": "", + "restore_button": "Restaurar", "confirm_restore": "¿Quiere restaurar esta revisión? Esto sobrescribirá el título actual y el contenido de la nota con esta revisión.", - "delete_button": "", + "delete_button": "Eliminar", "confirm_delete": "¿Quieres eliminar esta revisión?", "revisions_deleted": "Se han eliminado las revisiones de nota.", "revision_restored": "Se ha restaurado la revisión de nota.", @@ -588,6 +591,7 @@ "sat": "Sáb", "sun": "Dom", "cannot_find_day_note": "No se puede encontrar la nota del día", + "cannot_find_week_note": "No se puede encontrar la nota de la semana", "january": "Enero", "febuary": "Febrero", "march": "Marzo", @@ -639,7 +643,7 @@ "reload_frontend": "Recargar interfaz", "show_hidden_subtree": "Mostrar subárbol oculto", "show_help": "Mostrar ayuda", - "about": "Acerca de TriliumNext Notes", + "about": "Acerca de Trilium Notes", "logout": "Cerrar sesión", "show-cheatsheet": "Mostrar hoja de trucos", "toggle-zen-mode": "Modo Zen" @@ -1121,6 +1125,148 @@ "layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)", "layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo." }, + "ai_llm": { + "not_started": "No iniciado", + "title": "IA y ajustes de embeddings", + "processed_notes": "Notas procesadas", + "total_notes": "Notas totales", + "progress": "Progreso", + "queued_notes": "Notas en fila", + "failed_notes": "Notas fallidas", + "last_processed": "Última procesada", + "refresh_stats": "Recargar estadísticas", + "enable_ai_features": "Habilitar características IA/LLM", + "enable_ai_description": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM", + "openai_tab": "OpenAI", + "anthropic_tab": "Anthropic", + "voyage_tab": "Voyage AI", + "ollama_tab": "Ollama", + "enable_ai": "Habilitar características IA/LLM", + "enable_ai_desc": "Habilitar características de IA como resumen de notas, generación de contenido y otras capacidades LLM", + "provider_configuration": "Configuración de proveedor de IA", + "provider_precedence": "Precedencia de proveedor", + "provider_precedence_description": "Lista de proveedores en orden de precedencia separada por comas (p.e., 'openai,anthropic,ollama')", + "temperature": "Temperatura", + "temperature_description": "Controla la aleatoriedad de las respuestas (0 = determinista, 2 = aleatoriedad máxima)", + "system_prompt": "Mensaje de sistema", + "system_prompt_description": "Mensaje de sistema predeterminado utilizado para todas las interacciones de IA", + "openai_configuration": "Configuración de OpenAI", + "openai_settings": "Ajustes de OpenAI", + "api_key": "Clave API", + "url": "URL base", + "model": "Modelo", + "openai_api_key_description": "Tu clave API de OpenAI para acceder a sus servicios de IA", + "anthropic_api_key_description": "Tu clave API de Anthropic para acceder a los modelos Claude", + "default_model": "Modelo por defecto", + "openai_model_description": "Ejemplos: gpt-4o, gpt-4-turbo, gpt-3.5-turbo", + "base_url": "URL base", + "openai_url_description": "Por defecto: https://api.openai.com/v1", + "anthropic_settings": "Ajustes de Anthropic", + "anthropic_url_description": "URL base para la API de Anthropic (por defecto: https://api.anthropic.com)", + "anthropic_model_description": "Modelos Claude de Anthropic para el completado de chat", + "voyage_settings": "Ajustes de Voyage AI", + "ollama_settings": "Ajustes de Ollama", + "ollama_url_description": "URL para la API de Ollama (por defecto: http://localhost:11434)", + "ollama_model_description": "Modelo de Ollama a usar para el completado de chat", + "anthropic_configuration": "Configuración de Anthropic", + "voyage_configuration": "Configuración de Voyage AI", + "voyage_url_description": "Por defecto: https://api.voyageai.com/v1", + "ollama_configuration": "Configuración de Ollama", + "enable_ollama": "Habilitar Ollama", + "enable_ollama_description": "Habilitar Ollama para uso de modelo de IA local", + "ollama_url": "URL de Ollama", + "ollama_model": "Modelo de Ollama", + "refresh_models": "Refrescar modelos", + "refreshing_models": "Refrescando...", + "enable_automatic_indexing": "Habilitar indexado automático", + "rebuild_index": "Recrear índice", + "rebuild_index_error": "Error al comenzar la reconstrucción del índice. Consulte los registros para más detalles.", + "note_title": "Título de nota", + "error": "Error", + "last_attempt": "Último intento", + "actions": "Acciones", + "retry": "Reintentar", + "partial": "{{ percentage }}% completado", + "retry_queued": "Nota en la cola para reintento", + "retry_failed": "Hubo un fallo al poner en la cola a la nota para reintento", + "max_notes_per_llm_query": "Máximo de notas por consulta", + "max_notes_per_llm_query_description": "Número máximo de notas similares a incluir en el contexto IA", + "active_providers": "Proveedores activos", + "disabled_providers": "Proveedores deshabilitados", + "remove_provider": "Eliminar proveedor de la búsqueda", + "restore_provider": "Restaurar proveedor a la búsqueda", + "similarity_threshold": "Bias de similaridad", + "similarity_threshold_description": "Puntuación de similaridad mínima (0-1) para incluir notas en el contexto para consultas LLM", + "reprocess_index": "Reconstruir el índice de búsqueda", + "reprocessing_index": "Reconstruyendo...", + "reprocess_index_started": "La optimización de índice de búsqueda comenzó en segundo plano", + "reprocess_index_error": "Error al reconstruir el índice de búsqueda", + "index_rebuild_progress": "Progreso de reconstrucción de índice", + "index_rebuilding": "Optimizando índice ({{percentage}}%)", + "index_rebuild_complete": "Optimización de índice completa", + "index_rebuild_status_error": "Error al comprobar el estado de reconstrucción del índice", + "never": "Nunca", + "processing": "Procesando ({{percentage}}%)", + "incomplete": "Incompleto ({{percentage}}%)", + "complete": "Completo (100%)", + "refreshing": "Refrescando...", + "auto_refresh_notice": "Refrescar automáticamente cada {{seconds}} segundos", + "note_queued_for_retry": "Nota en la cola para reintento", + "failed_to_retry_note": "Hubo un fallo al reintentar nota", + "all_notes_queued_for_retry": "Todas las notas con fallo agregadas a la cola para reintento", + "failed_to_retry_all": "Hubo un fallo al reintentar notas", + "ai_settings": "Ajustes de IA", + "api_key_tooltip": "Clave API para acceder al servicio", + "empty_key_warning": { + "anthropic": "La clave API de Anthropic está vacía. Por favor, ingrese una clave API válida.", + "openai": "La clave API de OpenAI está vacía. Por favor, ingrese una clave API válida.", + "voyage": "La clave API de Voyage está vacía. Por favor, ingrese una clave API válida.", + "ollama": "La clave API de Ollama está vacía. Por favor, ingrese una clave API válida." + }, + "agent": { + "processing": "Procesando...", + "thinking": "Pensando...", + "loading": "Cargando...", + "generating": "Generando..." + }, + "name": "IA", + "openai": "OpenAI", + "use_enhanced_context": "Utilizar contexto mejorado", + "enhanced_context_description": "Provee a la IA con más contexto de la nota y sus notas relacionadas para obtener mejores respuestas", + "show_thinking": "Mostrar pensamiento", + "show_thinking_description": "Mostrar la cadena del proceso de pensamiento de la IA", + "enter_message": "Ingrese su mensaje...", + "error_contacting_provider": "Error al contactar con su proveedor de IA. Por favor compruebe sus ajustes y conexión a internet.", + "error_generating_response": "Error al generar respuesta de IA", + "index_all_notes": "Indexar todas las notas", + "index_status": "Estado de índice", + "indexed_notes": "Notas indexadas", + "indexing_stopped": "Indexado detenido", + "indexing_in_progress": "Indexado en progreso...", + "last_indexed": "Último indexado", + "n_notes_queued": "{{ count }} nota agregada a la cola para indexado", + "n_notes_queued_plural": "{{ count }} notas agregadas a la cola para indexado", + "note_chat": "Chat de nota", + "notes_indexed": "{{ count }} nota indexada", + "notes_indexed_plural": "{{ count }} notas indexadas", + "sources": "Fuentes", + "start_indexing": "Comenzar indexado", + "use_advanced_context": "Usar contexto avanzado", + "ollama_no_url": "Ollama no está configurado. Por favor ingrese una URL válida.", + "chat": { + "root_note_title": "Chats de IA", + "root_note_content": "Esta nota contiene tus conversaciones de chat de IA guardadas.", + "new_chat_title": "Nuevo chat", + "create_new_ai_chat": "Crear nuevo chat de IA" + }, + "create_new_ai_chat": "Crear nuevo chat de IA", + "configuration_warnings": "Hay algunos problemas con su configuración de IA. Por favor compruebe sus ajustes.", + "experimental_warning": "La característica de LLM aún es experimental - ha sido advertido.", + "selected_provider": "Proveedor seleccionado", + "selected_provider_description": "Elija el proveedor de IA para el chat y características de completado", + "select_model": "Seleccionar modelo...", + "select_provider": "Seleccionar proveedor..." + }, "zoom_factor": { "title": "Factor de zoom (solo versión de escritorio)", "description": "El zoom también se puede controlar con los atajos CTRL+- y CTRL+=." @@ -1236,12 +1382,26 @@ "label": "Tamaño para modo de solo lectura automático (notas de texto)", "unit": "caracteres" }, + "custom_date_time_format": { + "title": "Formato de fecha/hora personalizada", + "description": "Personalizar el formado de fecha y la hora insertada vía o la barra de herramientas. Véa la documentación de Day.js para más tokens de formato disponibles.", + "format_string": "Cadena de formato:", + "formatted_time": "Fecha/hora personalizada:" + }, "i18n": { "title": "Localización", "language": "Idioma", "first-day-of-the-week": "Primer día de la semana", "sunday": "Domingo", - "monday": "Lunes" + "monday": "Lunes", + "first-week-of-the-year": "Primer semana del año", + "first-week-contains-first-day": "Primer semana que contiene al primer día del año", + "first-week-contains-first-thursday": "Primer semana que contiene al primer jueves del año", + "first-week-has-minimum-days": "Primer semana que contiene un mínimo de días", + "min-days-in-first-week": "Días mínimos en la primer semana", + "first-week-info": "Primer semana que contiene al primer jueves del año está basado en el estándarISO 8601.", + "first-week-warning": "Cambiar las opciones de primer semana puede causar duplicados con las Notas Semanales existentes y las Notas Semanales existentes no serán actualizadas respectivamente.", + "formatting-locale": "Fecha y formato de número" }, "backup": { "automatic_backup": "Copia de seguridad automática", @@ -1308,6 +1468,39 @@ "password_mismatch": "Las nuevas contraseñas no son las mismas.", "password_changed_success": "La contraseña ha sido cambiada. Trilium se recargará después de presionar Aceptar." }, + "multi_factor_authentication": { + "title": "Autenticación Multi-Factor", + "description": "La autenticación multifactor (MFA) agrega una capa adicional de seguridad a su cuenta. En lugar de solo ingresar una contraseña para iniciar sesión, MFA requiere que proporcione una o más pruebas adicionales para verificar su identidad. De esta manera, incluso si alguien se apodera de su contraseña, aún no puede acceder a su cuenta sin la segunda pieza de información. Es como agregar una cerradura adicional a su puerta, lo que hace que sea mucho más difícil para cualquier otra persona entrar.

Por favor siga las instrucciones a continuación para habilitar MFA. Si no lo configura correctamente, el inicio de sesión volverá a solo contraseña.", + "mfa_enabled": "Habilitar la autenticación multifactor", + "mfa_method": "Método MFA", + "electron_disabled": "Actualmente la autenticación multifactor no está soportada en la compilación de escritorio.", + "totp_title": "Contraseña de un solo uso basada en el tiempo (TOTP)", + "totp_description": "TOTP (contraseña de un solo uso basada en el tiempo) es una característica de seguridad que genera un código temporal único que cambia cada 30 segundos. Utiliza este código, junto con su contraseña para iniciar sesión en su cuenta, lo que hace que sea mucho más difícil para cualquier otra persona acceder a ella.", + "totp_secret_title": "Generar secreto TOTP", + "totp_secret_generate": "Generar secreto TOTP", + "totp_secret_regenerate": "Regenerar secreto TOTP", + "no_totp_secret_warning": "Para habilitar TOTP, primero debe de generar un secreto TOTP.", + "totp_secret_description_warning": "Después de generar un nuevo secreto TOTP, le será requerido que inicie sesión otra vez con el nuevo secreto TOTP.", + "totp_secret_generated": "Secreto TOTP generado", + "totp_secret_warning": "Por favor guarde el secreto generado en una ubicación segura. No será mostrado de nuevo.", + "totp_secret_regenerate_confirm": "¿Está seguro que desea regenerar el secreto TOTP? Esto va a invalidar el secreto TOTP previo y todos los códigos de recuperación existentes.", + "recovery_keys_title": "Claves de recuperación para un solo inicio de sesión", + "recovery_keys_description": "Las claves de recuperación para un solo inicio de sesión son usadas para iniciar sesión incluso cuando no puede acceder a los códigos de su autentificador.", + "recovery_keys_description_warning": "Las claves de recuperación no son mostrada de nuevo después de dejar esta página, manténgalas en un lugar seguro.
Después de que una clave de recuperación es utilizada ya no puede utilizarse de nuevo.", + "recovery_keys_error": "Error al generar códigos de recuperación", + "recovery_keys_no_key_set": "No hay códigos de recuperación establecidos", + "recovery_keys_generate": "Generar códigos de recuperación", + "recovery_keys_regenerate": "Regenerar códigos de recuperación", + "recovery_keys_used": "Usado: {{date}}", + "recovery_keys_unused": "El código de recuperación {{index}} está sin usar", + "oauth_title": "OAuth/OpenID", + "oauth_description": "OpenID es una forma estandarizada de permitirle iniciar sesión en sitios web utilizando una cuenta de otro servicio, como Google, para verificar su identidad. Siga estas instrucciones para configurar un servicio OpenID a través de Google.", + "oauth_description_warning": "Para habilitar OAuth/OpenID, necesita establecer la URL base de OAuth/OpenID, ID de cliente y secreto de cliente en el archivo config.ini y reiniciar la aplicación. Si desea establecerlas desde variables de ambiente, por favor establezca TRILIUM_OAUTH_BASE_URL, TRILIUM_OAUTH_CLIENT_ID y TRILIUM_OAUTH_CLIENT_SECRET.", + "oauth_missing_vars": "Ajustes faltantes: {{variables}}", + "oauth_user_account": "Cuenta de usuario: ", + "oauth_user_email": "Correo electrónico de usuario: ", + "oauth_user_not_logged_in": "¡No ha iniciado sesión!" + }, "shortcuts": { "keyboard_shortcuts": "Atajos de teclado", "multiple_shortcuts": "Varios atajos para la misma acción se pueden separar mediante comas.", @@ -1431,7 +1624,9 @@ "widget": "Widget", "confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?", "geo-map": "Mapa Geo", - "beta-feature": "Beta" + "beta-feature": "Beta", + "ai-chat": "Chat de IA", + "task-list": "Lista de tareas" }, "protect_note": { "toggle-on": "Proteger la nota", @@ -1541,7 +1736,9 @@ }, "clipboard": { "cut": "La(s) notas(s) han sido cortadas al portapapeles.", - "copied": "La(s) notas(s) han sido copiadas al portapapeles." + "copied": "La(s) notas(s) han sido copiadas al portapapeles.", + "copy_failed": "No se puede copiar al portapapeles debido a problemas de permisos.", + "copy_success": "Copiado al portapapeles." }, "entrypoints": { "note-revision-created": "Una revisión de nota ha sido creada.", @@ -1584,7 +1781,7 @@ "auto-detect-language": "Detectado automáticamente" }, "highlighting": { - "title": "", + "title": "Bloques de código", "description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.", "color-scheme": "Esquema de color" }, @@ -1592,7 +1789,8 @@ "word_wrapping": "Ajuste de palabras", "theme_none": "Sin resaltado de sintaxis", "theme_group_light": "Temas claros", - "theme_group_dark": "Temas oscuros" + "theme_group_dark": "Temas oscuros", + "copy_title": "Copiar al portapapeles" }, "classic_editor_toolbar": { "title": "Formato" @@ -1713,5 +1911,22 @@ }, "png_export_button": { "button_title": "Exportar diagrama como PNG" + }, + "svg": { + "export_to_png": "El diagrama no pudo ser exportado a PNG." + }, + "code_theme": { + "title": "Apariencia", + "word_wrapping": "Ajuste de palabras", + "color-scheme": "Esquema de color" + }, + "cpu_arch_warning": { + "title": "Por favor descargue la versión ARM64", + "message_macos": "TriliumNext está siendo ejecutado bajo traducción Rosetta 2, lo que significa que está usando la versión Intel (x64) en Apple Silicon Mac. Esto impactará significativamente en el rendimiento y la vida de la batería.", + "message_windows": "TriliumNext está siendo ejecutado bajo emulación, lo que significa que está usando la version Intel (x64) en Windows en un dispositivo ARM. Esto impactará significativamente en el rendimiento y la vida de la batería.", + "recommendation": "Para la mejor experiencia, por favor descargue la versión nativa ARM64 de TriliumNext desde nuestra página de lanzamientos.", + "download_link": "Descargar versión nativa", + "continue_anyway": "Continuar de todas maneras", + "dont_show_again": "No mostrar esta advertencia otra vez" } } diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index 74335e936..2ea871fc9 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "À propos de TriliumNext Notes", + "title": "À propos de Trilium Notes", "close": "Fermer", "homepage": "Page d'accueil :", "app_version": "Version de l'application :", @@ -639,7 +639,7 @@ "reload_frontend": "Recharger l'interface", "show_hidden_subtree": "Afficher le Sous-arbre caché", "show_help": "Afficher l'aide", - "about": "À propos de TriliumNext Notes", + "about": "À propos de Trilium Notes", "logout": "Déconnexion", "show-cheatsheet": "Afficher l'aide rapide", "toggle-zen-mode": "Zen Mode" diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 25b073e9e..537b43420 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "Despre TriliumNext Notes", + "title": "Despre Trilium Notes", "homepage": "Site web:", "app_version": "Versiune aplicație:", "db_version": "Versiune bază de date:", @@ -572,7 +572,7 @@ "system-default": "Fontul predefinit al sistemului" }, "global_menu": { - "about": "Despre TriliumNext Notes", + "about": "Despre Trilium Notes", "advanced": "Opțiuni avansate", "configure_launchbar": "Configurează bara de lansare", "logout": "Deautentificare", diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index c25504510..8a93a2281 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -1,6 +1,6 @@ { "about": { - "title": "關於 TriliumNext Notes", + "title": "關於 Trilium Notes", "homepage": "項目主頁:", "app_version": "軟件版本:", "db_version": "資料庫版本:", diff --git a/apps/client/src/types-assets.d.ts b/apps/client/src/types-assets.d.ts index 010ec6b44..e80532517 100644 --- a/apps/client/src/types-assets.d.ts +++ b/apps/client/src/types-assets.d.ts @@ -8,4 +8,9 @@ declare module "*?url" { export default path; } +declare module "*?raw" { + var content: string; + export default content; +} + declare module "boxicons/css/boxicons.min.css" { } diff --git a/apps/client/src/vite-env.d.ts b/apps/client/src/vite-env.d.ts new file mode 100644 index 000000000..d6d562031 --- /dev/null +++ b/apps/client/src/vite-env.d.ts @@ -0,0 +1,16 @@ +/// + +interface ViteTypeOptions { + strictImportMetaEnv: unknown +} + +interface ImportMetaEnv { + /** The license key for CKEditor premium features. */ + readonly VITE_CKEDITOR_KEY?: string; + /** Whether to enable the CKEditor inspector (see https://ckeditor.com/docs/ckeditor5/latest/framework/develpment-tools/inspector.html). */ + readonly VITE_CKEDITOR_ENABLE_INSPECTOR?: "true" | "false"; +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 08bb764fc..9017673f3 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -11,6 +11,7 @@ import utils from "../../services/utils.js"; import shortcutService from "../../services/shortcuts.js"; import appContext from "../../components/app_context.js"; import type { Attribute } from "../../services/attribute_parser.js"; +import { focusSavedElement, saveFocusedElement } from "../../services/focus.js"; const TPL = /*html*/`
@@ -483,7 +484,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { return; } - utils.saveFocusedElement(); + saveFocusedElement(); this.attrType = this.getAttrType(attribute); @@ -605,7 +606,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.hide(); - utils.focusSavedElement(); + focusSavedElement(); } async cancelAndClose() { @@ -613,7 +614,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { this.hide(); - utils.focusSavedElement(); + focusSavedElement(); } userEditedAttribute() { diff --git a/apps/client/src/widgets/dialogs/about.ts b/apps/client/src/widgets/dialogs/about.ts index 2c364d756..06cf118ec 100644 --- a/apps/client/src/widgets/dialogs/about.ts +++ b/apps/client/src/widgets/dialogs/about.ts @@ -4,6 +4,7 @@ import BasicWidget from "../basic_widget.js"; import openService from "../../services/open.js"; import server from "../../services/server.js"; import utils from "../../services/utils.js"; +import { openDialog } from "../../services/dialog.js"; interface AppInfo { appVersion: string; @@ -111,6 +112,6 @@ export default class AboutDialog extends BasicWidget { async openAboutDialogEvent() { await this.refresh(); - utils.openDialog(this.$widget); + openDialog(this.$widget); } } diff --git a/apps/client/src/widgets/dialogs/add_link.ts b/apps/client/src/widgets/dialogs/add_link.ts index 14defb082..d7758c92d 100644 --- a/apps/client/src/widgets/dialogs/add_link.ts +++ b/apps/client/src/widgets/dialogs/add_link.ts @@ -1,11 +1,11 @@ import { t } from "../../services/i18n.js"; import treeService from "../../services/tree.js"; import noteAutocompleteService from "../../services/note_autocomplete.js"; -import utils from "../../services/utils.js"; import BasicWidget from "../basic_widget.js"; import type { Suggestion } from "../../services/note_autocomplete.js"; import type { default as TextTypeWidget } from "../type_widgets/editable_text.js"; import type { EventData } from "../../components/app_context.js"; +import { openDialog } from "../../services/dialog.js"; const TPL = /*html*/`